From 7e97a79ac20fcfeeef870d57ad779ac7704c2db8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 18:42:16 -0700 Subject: [PATCH 01/34] wip --- .../src/mill/main/client/ServerFiles.java | 1 + runner/src/mill/runner/MillServerMain.scala | 70 ++++++++++++++----- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/main/client/src/mill/main/client/ServerFiles.java b/main/client/src/mill/main/client/ServerFiles.java index d4ee7d11d48..20e280a857f 100644 --- a/main/client/src/mill/main/client/ServerFiles.java +++ b/main/client/src/mill/main/client/ServerFiles.java @@ -5,6 +5,7 @@ * and documentation about what they do */ public class ServerFiles { + final public static String serverId = "serverId"; final public static String sandbox = "sandbox"; /** diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index 103558e74eb..da1d26024d9 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -95,9 +95,14 @@ class Server[T]( ) { val originalStdout = System.out + val serverId = scala.util.Random.nextLong().toString + def serverLog(s: String) = os.write.append(lockBase / ServerFiles.serverLog, s + "\n") def run(): Unit = { + val initialSystemProperties = sys.props.toMap Server.tryLockBlock(locks.processLock) { + watchServerIdFile() + var running = true while (running) { @@ -108,10 +113,11 @@ class Server[T]( // Use relative path because otherwise the full path might be too long for the socket API val addr = AFUNIXSocketAddress.of(socketPath.relativeTo(os.pwd).toNIO.toFile) + serverLog("listening on socket") val serverSocket = AFUNIXServerSocket.bindOn(addr) val socketClose = () => serverSocket.close() - val sockOpt = Server.interruptWith( + val sockOpt = interruptWith( "MillSocketTimeoutInterruptThread", acceptTimeoutMillis, socketClose(), @@ -122,8 +128,9 @@ class Server[T]( case None => running = false case Some(sock) => try { + serverLog("handling run") try handleRun(sock, initialSystemProperties) - catch { case e: Throwable => e.printStackTrace(originalStdout) } + catch { case e: Throwable => serverLog(e + "\n" + e.getStackTrace.mkString("\n"))} finally sock.close(); } finally serverSocket.close() } @@ -141,6 +148,7 @@ class Server[T]( pumperThread.start() pipedInput } + def handleRun(clientSocket: Socket, initialSystemProperties: Map[String, String]): Unit = { val currentOutErr = clientSocket.getOutputStream @@ -225,23 +233,28 @@ class Server[T]( } finally ProxyStream.sendEnd(currentOutErr) // Send a termination } -} - -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 watchServerIdFile() = { + os.write.over(lockBase / ServerFiles.serverId, serverId) + val serverIdThread = new Thread( + () => { + while (true){ + Thread.sleep(100) + Try(os.read(lockBase / ServerFiles.serverId)).toOption match{ + case None => + serverLog("serverId file missing, exiting") + System.exit(0) + case Some(s) => + if (s != serverId){ + serverLog(s"serverId file contents $s does not match serverId $serverId, exiting") + System.exit(0) + } + } + } + }, + "Server ID Checker Thread" + ) + serverIdThread.start() } def interruptWith[T](threadName: String, millis: Int, close: => Unit, t: => T): Option[T] = { @@ -253,6 +266,7 @@ object Server { catch { case t: InterruptedException => /* Do Nothing */ } if (interrupt) { interrupted = true + serverLog(s"Interrupting after ${millis}ms") close } }, @@ -274,3 +288,23 @@ object Server { } } } + +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() + } + } + + +} From f79540a1a0698b1b2d83d2b074e965bc859fd9a7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 19:10:50 -0700 Subject: [PATCH 02/34] compiles --- build.sc | 7 +- .../src/mill/main/server/MillServerMain.scala | 161 +++++++++++++ .../mill/main/server}/ClientServerTests.scala | 18 +- runner/src/mill/runner/MillServerMain.scala | 218 +++--------------- 4 files changed, 209 insertions(+), 195 deletions(-) create mode 100644 main/server/src/mill/main/server/MillServerMain.scala rename {runner/test/src/mill/runner => main/server/test/src/mill/main/server}/ClientServerTests.scala (94%) diff --git a/build.sc b/build.sc index a645d6a5626..d3a09a47af1 100644 --- a/build.sc +++ b/build.sc @@ -762,6 +762,9 @@ object main extends MillStableScalaModule with BuildInfo { } } + object server extends MillPublishScalaModule { + def moduleDeps = Seq(client, api) + } object graphviz extends MillPublishScalaModule { def moduleDeps = Seq(main, scalalib) def ivyDeps = Agg(Deps.graphvizJava, Deps.jgraphtCore) @@ -1545,7 +1548,9 @@ def launcherScript( } object runner extends MillPublishScalaModule { - def moduleDeps = Seq(scalalib, scalajslib, scalanativelib, bsp, linenumbers, main.codesig) + def moduleDeps = Seq( + scalalib, scalajslib, scalanativelib, bsp, linenumbers, main.codesig, main.server + ) def skipPreviousVersions: T[Seq[String]] = Seq("0.11.0-M7") object linenumbers extends MillPublishScalaModule { diff --git a/main/server/src/mill/main/server/MillServerMain.scala b/main/server/src/mill/main/server/MillServerMain.scala new file mode 100644 index 00000000000..36da53a8268 --- /dev/null +++ b/main/server/src/mill/main/server/MillServerMain.scala @@ -0,0 +1,161 @@ +package mill.main.server + +import sun.misc.{Signal, SignalHandler} + +import java.io._ +import java.net.Socket +import scala.jdk.CollectionConverters._ +import org.newsclub.net.unix.AFUNIXServerSocket +import org.newsclub.net.unix.AFUNIXSocketAddress +import mill.main.client._ +import mill.api.{SystemStreams, internal} +import mill.main.client.ProxyStream.Output +import mill.main.client.lock.{Lock, Locks} + +import scala.util.Try + + +abstract class MillServerMain[T]( + serverDir: os.Path, + interruptServer: () => Unit, + acceptTimeoutMillis: Int, + locks: Locks +) { + def stateCache0: T + var stateCache = stateCache0 + def main0( + args: Array[String], + stateCache: T, + mainInteractive: Boolean, + streams: SystemStreams, + env: Map[String, String], + setIdle: Boolean => Unit, + systemProperties: Map[String, String], + initialSystemProperties: Map[String, String] + ): (Boolean, T) + + val originalStdout = System.out + val serverId = scala.util.Random.nextLong().toString + def serverLog(s: String) = os.write.append(serverDir / ServerFiles.serverLog, s + "\n") + def run(): Unit = { + val initialSystemProperties = sys.props.toMap + + Server.tryLockBlock(locks.processLock) { + + watchServerIdFile() + + var running = true + while (running) { + + serverLog("listening on socket") + val serverSocket = bindSocket() + val sockOpt = interruptWithTimeout(() => serverSocket.close(), () => serverSocket.accept()) + + sockOpt match { + case None => running = false + case Some(sock) => + try { + serverLog("handling run") + try handleRun(sock, initialSystemProperties) + catch { case e: Throwable => serverLog(e + "\n" + e.getStackTrace.mkString("\n"))} + finally sock.close(); + } finally serverSocket.close() + } + } + }.getOrElse(throw new Exception("Mill server process already present, exiting")) + } + + def bindSocket() = { + val socketPath = os.Path(ServerFiles.pipe(serverDir.toString())) + os.remove.all(socketPath) + + // Use relative path because otherwise the full path might be too long for the socket API + val addr = AFUNIXSocketAddress.of(socketPath.relativeTo(os.pwd).toNIO.toFile) + AFUNIXServerSocket.bindOn(addr) + } + + def proxyInputStreamThroughPumper(in: InputStream): PipedInputStream = { + val pipedInput = new PipedInputStream() + val pipedOutput = new PipedOutputStream() + pipedOutput.connect(pipedInput) + val pumper = new InputPumper(() => in, () => pipedOutput, false) + val pumperThread = new Thread(pumper, "proxyInputStreamThroughPumper") + pumperThread.setDaemon(true) + pumperThread.start() + pipedInput + } + + def handleRun(clientSocket: Socket, initialSystemProperties: Map[String, String]): Unit + + def watchServerIdFile() = { + os.write.over(serverDir / ServerFiles.serverId, serverId) + val serverIdThread = new Thread( + () => { + while (true){ + Thread.sleep(100) + Try(os.read(serverDir / ServerFiles.serverId)).toOption match{ + case None => + serverLog("serverId file missing, exiting") + System.exit(0) + case Some(s) => + if (s != serverId){ + serverLog(s"serverId file contents $s does not match serverId $serverId, exiting") + System.exit(0) + } + } + } + }, + "Server ID Checker Thread" + ) + serverIdThread.start() + } + + def interruptWithTimeout[T](close: () => Unit, t: () => T): Option[T] = { + @volatile var interrupt = true + @volatile var interrupted = false + val thread = new Thread( + () => { + try Thread.sleep(acceptTimeoutMillis) + catch { case t: InterruptedException => /* Do Nothing */ } + if (interrupt) { + interrupted = true + serverLog(s"Interrupting after ${acceptTimeoutMillis}ms") + close() + } + }, + "MillSocketTimeoutInterruptThread", + ) + + thread.start() + try { + val res = + try Some(t()) + catch { case e: Throwable => None } + + if (interrupted) None + else res + + } finally { + thread.interrupt() + interrupt = false + } + } +} + +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() + } + } +} diff --git a/runner/test/src/mill/runner/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala similarity index 94% rename from runner/test/src/mill/runner/ClientServerTests.scala rename to main/server/test/src/mill/main/server/ClientServerTests.scala index 5eb24f6a765..4537902ab71 100644 --- a/runner/test/src/mill/runner/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -1,4 +1,4 @@ -package mill.runner +package mill.main.server import java.io._ import mill.main.client.Util @@ -8,7 +8,8 @@ import mill.api.SystemStreams import scala.jdk.CollectionConverters._ import utest._ -class EchoServer extends MillServerMain[Option[Int]] { +class EchoServer(tmpDir: os.Path, locks: Locks) + extends MillServerMain[Option[Int]](tmpDir, () => (), 1000, locks) with Runnable{ def stateCache0 = None def main0( args: Array[String], @@ -39,6 +40,9 @@ class EchoServer extends MillServerMain[Option[Int]] { streams.err.flush() (true, None) } + + def handleRun(clientSocket: java.net.Socket, + initialSystemProperties: Map[String,String]): Unit = ??? } object ClientServerTests extends TestSuite { @@ -59,15 +63,7 @@ object ClientServerTests extends TestSuite { } def spawnEchoServer(tmpDir: os.Path, locks: Locks): Unit = { - new Thread(() => - new Server( - tmpDir, - new EchoServer(), - () => (), - 1000, - locks - ).run() - ).start() + new Thread(new EchoServer(tmpDir, locks)).start() } def runClientAux( diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index da1d26024d9..4c0669ba934 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -7,33 +7,13 @@ import java.net.Socket import scala.jdk.CollectionConverters._ import org.newsclub.net.unix.AFUNIXServerSocket import org.newsclub.net.unix.AFUNIXSocketAddress -import mill.main.BuildInfo import mill.main.client._ import mill.api.{SystemStreams, internal} import mill.main.client.ProxyStream.Output import mill.main.client.lock.{Lock, Locks} - import scala.util.Try -@internal -trait MillServerMain[T] { - def stateCache0: T - var stateCache = stateCache0 - def main0( - args: Array[String], - stateCache: T, - mainInteractive: Boolean, - streams: SystemStreams, - env: Map[String, String], - setIdle: Boolean => Unit, - systemProperties: Map[String, String], - initialSystemProperties: Map[String, String] - ): (Boolean, T) -} - -@internal -object MillServerMain extends MillServerMain[RunnerState] { - def stateCache0 = RunnerState.empty +object MillServerMain{ def main(args0: Array[String]): Unit = { // Disable SIGINT interrupt signal in the Mill server. // @@ -52,102 +32,47 @@ object MillServerMain extends MillServerMain[RunnerState] { val acceptTimeoutMillis = Try(System.getProperty("mill.server_timeout").toInt).getOrElse(5 * 60 * 1000) // 5 minutes - new Server( + new MillServerMain( lockBase = os.Path(args0(0)), - this, () => System.exit(Util.ExitServerCodeWhenIdle()), acceptTimeoutMillis = acceptTimeoutMillis, Locks.files(args0(0)) ).run() } +} +class MillServerMain( + lockBase: os.Path, + interruptServer: () => Unit, + acceptTimeoutMillis: Int, + locks: Locks + ) + extends mill.main.server.MillServerMain[RunnerState](lockBase, interruptServer, acceptTimeoutMillis, locks) { + def stateCache0 = RunnerState.empty + def main0( - args: Array[String], - stateCache: RunnerState, - mainInteractive: Boolean, - streams: SystemStreams, - env: Map[String, String], - setIdle: Boolean => Unit, - userSpecifiedProperties: Map[String, String], - initialSystemProperties: Map[String, String] - ): (Boolean, RunnerState) = { + args: Array[String], + stateCache: RunnerState, + mainInteractive: Boolean, + streams: SystemStreams, + env: Map[String, String], + setIdle: Boolean => Unit, + userSpecifiedProperties: Map[String, String], + initialSystemProperties: Map[String, String] + ): (Boolean, RunnerState) = { try MillMain.main0( - args = args, - stateCache = stateCache, - mainInteractive = mainInteractive, - streams0 = streams, - bspLog = None, - env = env, - setIdle = setIdle, - userSpecifiedProperties0 = userSpecifiedProperties, - initialSystemProperties = initialSystemProperties - ) + args = args, + stateCache = stateCache, + mainInteractive = mainInteractive, + streams0 = streams, + bspLog = None, + env = env, + setIdle = setIdle, + userSpecifiedProperties0 = userSpecifiedProperties, + initialSystemProperties = initialSystemProperties + ) catch MillMain.handleMillException(streams.err, stateCache) } -} - -class Server[T]( - lockBase: os.Path, - sm: MillServerMain[T], - interruptServer: () => Unit, - acceptTimeoutMillis: Int, - locks: Locks -) { - - val originalStdout = System.out - val serverId = scala.util.Random.nextLong().toString - def serverLog(s: String) = os.write.append(lockBase / ServerFiles.serverLog, s + "\n") - def run(): Unit = { - - val initialSystemProperties = sys.props.toMap - Server.tryLockBlock(locks.processLock) { - watchServerIdFile() - - var running = true - while (running) { - - val socketPath = os.Path(ServerFiles.pipe(lockBase.toString())) - - os.remove.all(socketPath) - - // Use relative path because otherwise the full path might be too long for the socket API - val addr = - AFUNIXSocketAddress.of(socketPath.relativeTo(os.pwd).toNIO.toFile) - serverLog("listening on socket") - val serverSocket = AFUNIXServerSocket.bindOn(addr) - val socketClose = () => serverSocket.close() - - val sockOpt = interruptWith( - "MillSocketTimeoutInterruptThread", - acceptTimeoutMillis, - socketClose(), - serverSocket.accept() - ) - - sockOpt match { - case None => running = false - case Some(sock) => - try { - serverLog("handling run") - try handleRun(sock, initialSystemProperties) - catch { case e: Throwable => serverLog(e + "\n" + e.getStackTrace.mkString("\n"))} - finally sock.close(); - } finally serverSocket.close() - } - } - }.getOrElse(throw new Exception("PID already present")) - } - - def proxyInputStreamThroughPumper(in: InputStream): PipedInputStream = { - val pipedInput = new PipedInputStream() - val pipedOutput = new PipedOutputStream() - pipedOutput.connect(pipedInput) - val pumper = new InputPumper(() => in, () => pipedOutput, false) - val pumperThread = new Thread(pumper, "proxyInputStreamThroughPumper") - pumperThread.setDaemon(true) - pumperThread.start() - pipedInput - } def handleRun(clientSocket: Socket, initialSystemProperties: Map[String, String]): Unit = { @@ -186,9 +111,9 @@ class Server[T]( val t = new Thread( () => try { - val (result, newStateCache) = sm.main0( + val (result, newStateCache) = main0( args, - sm.stateCache, + stateCache, interactive, new SystemStreams(stdout, stderr, proxiedSocketInput), env.asScala.toMap, @@ -197,7 +122,7 @@ class Server[T]( initialSystemProperties ) - sm.stateCache = newStateCache + stateCache = newStateCache os.write.over( lockBase / ServerFiles.exitCode, (if (result) 0 else 1).toString.getBytes() @@ -234,77 +159,4 @@ class Server[T]( } finally ProxyStream.sendEnd(currentOutErr) // Send a termination } - def watchServerIdFile() = { - os.write.over(lockBase / ServerFiles.serverId, serverId) - val serverIdThread = new Thread( - () => { - while (true){ - Thread.sleep(100) - Try(os.read(lockBase / ServerFiles.serverId)).toOption match{ - case None => - serverLog("serverId file missing, exiting") - System.exit(0) - case Some(s) => - if (s != serverId){ - serverLog(s"serverId file contents $s does not match serverId $serverId, exiting") - System.exit(0) - } - } - } - }, - "Server ID Checker Thread" - ) - serverIdThread.start() - } - - 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 - serverLog(s"Interrupting after ${millis}ms") - 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 - } - } -} - -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() - } - } - - -} +} \ No newline at end of file From c510c4fba277f0a53ff4d9ca4c059925e071d218 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 19:16:05 -0700 Subject: [PATCH 03/34] wip --- .../{MillServerMain.scala => Server.scala} | 12 +------ .../mill/main/server/ClientServerTests.scala | 2 +- runner/src/mill/runner/MillServerMain.scala | 35 ++++--------------- 3 files changed, 9 insertions(+), 40 deletions(-) rename main/server/src/mill/main/server/{MillServerMain.scala => Server.scala} (91%) diff --git a/main/server/src/mill/main/server/MillServerMain.scala b/main/server/src/mill/main/server/Server.scala similarity index 91% rename from main/server/src/mill/main/server/MillServerMain.scala rename to main/server/src/mill/main/server/Server.scala index 36da53a8268..5380ad4f5e7 100644 --- a/main/server/src/mill/main/server/MillServerMain.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -15,7 +15,7 @@ import mill.main.client.lock.{Lock, Locks} import scala.util.Try -abstract class MillServerMain[T]( +abstract class Server[T]( serverDir: os.Path, interruptServer: () => Unit, acceptTimeoutMillis: Int, @@ -23,16 +23,6 @@ abstract class MillServerMain[T]( ) { def stateCache0: T var stateCache = stateCache0 - def main0( - args: Array[String], - stateCache: T, - mainInteractive: Boolean, - streams: SystemStreams, - env: Map[String, String], - setIdle: Boolean => Unit, - systemProperties: Map[String, String], - initialSystemProperties: Map[String, String] - ): (Boolean, T) val originalStdout = System.out val serverId = scala.util.Random.nextLong().toString diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 4537902ab71..56d062c8768 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -9,7 +9,7 @@ import scala.jdk.CollectionConverters._ import utest._ class EchoServer(tmpDir: os.Path, locks: Locks) - extends MillServerMain[Option[Int]](tmpDir, () => (), 1000, locks) with Runnable{ + extends Server[Option[Int]](tmpDir, () => (), 1000, locks) with Runnable{ def stateCache0 = None def main0( args: Array[String], diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index 4c0669ba934..ac186959ed9 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -46,34 +46,10 @@ class MillServerMain( acceptTimeoutMillis: Int, locks: Locks ) - extends mill.main.server.MillServerMain[RunnerState](lockBase, interruptServer, acceptTimeoutMillis, locks) { + extends mill.main.server.Server[RunnerState](lockBase, interruptServer, acceptTimeoutMillis, locks) { def stateCache0 = RunnerState.empty - def main0( - args: Array[String], - stateCache: RunnerState, - mainInteractive: Boolean, - streams: SystemStreams, - env: Map[String, String], - setIdle: Boolean => Unit, - userSpecifiedProperties: Map[String, String], - initialSystemProperties: Map[String, String] - ): (Boolean, RunnerState) = { - try MillMain.main0( - args = args, - stateCache = stateCache, - mainInteractive = mainInteractive, - streams0 = streams, - bspLog = None, - env = env, - setIdle = setIdle, - userSpecifiedProperties0 = userSpecifiedProperties, - initialSystemProperties = initialSystemProperties - ) - catch MillMain.handleMillException(streams.err, stateCache) - } - def handleRun(clientSocket: Socket, initialSystemProperties: Map[String, String]): Unit = { val currentOutErr = clientSocket.getOutputStream @@ -111,16 +87,19 @@ class MillServerMain( val t = new Thread( () => try { - val (result, newStateCache) = main0( + val streams = new SystemStreams(stdout, stderr, proxiedSocketInput) + val (result, newStateCache) = try MillMain.main0( args, stateCache, interactive, - new SystemStreams(stdout, stderr, proxiedSocketInput), + streams, + None, env.asScala.toMap, idle = _, userSpecifiedProperties.asScala.toMap, initialSystemProperties - ) + ) catch MillMain.handleMillException(streams.err, stateCache) + stateCache = newStateCache os.write.over( From 25108ee4fd7f726ec76f009fd42b999db39373c4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 19:19:43 -0700 Subject: [PATCH 04/34] fmt --- main/server/src/mill/main/server/Server.scala | 19 ++++---- .../mill/main/server/ClientServerTests.scala | 6 +-- runner/src/mill/runner/MillServerMain.scala | 46 ++++++++++--------- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index 5380ad4f5e7..65f689a5a35 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -14,12 +14,11 @@ import mill.main.client.lock.{Lock, Locks} import scala.util.Try - abstract class Server[T]( - serverDir: os.Path, - interruptServer: () => Unit, - acceptTimeoutMillis: Int, - locks: Locks + serverDir: os.Path, + interruptServer: () => Unit, + acceptTimeoutMillis: Int, + locks: Locks ) { def stateCache0: T var stateCache = stateCache0 @@ -47,7 +46,7 @@ abstract class Server[T]( try { serverLog("handling run") try handleRun(sock, initialSystemProperties) - catch { case e: Throwable => serverLog(e + "\n" + e.getStackTrace.mkString("\n"))} + catch { case e: Throwable => serverLog(e + "\n" + e.getStackTrace.mkString("\n")) } finally sock.close(); } finally serverSocket.close() } @@ -81,14 +80,14 @@ abstract class Server[T]( os.write.over(serverDir / ServerFiles.serverId, serverId) val serverIdThread = new Thread( () => { - while (true){ + while (true) { Thread.sleep(100) - Try(os.read(serverDir / ServerFiles.serverId)).toOption match{ + Try(os.read(serverDir / ServerFiles.serverId)).toOption match { case None => serverLog("serverId file missing, exiting") System.exit(0) case Some(s) => - if (s != serverId){ + if (s != serverId) { serverLog(s"serverId file contents $s does not match serverId $serverId, exiting") System.exit(0) } @@ -113,7 +112,7 @@ abstract class Server[T]( close() } }, - "MillSocketTimeoutInterruptThread", + "MillSocketTimeoutInterruptThread" ) thread.start() diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 56d062c8768..39dee39d058 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -9,7 +9,7 @@ import scala.jdk.CollectionConverters._ import utest._ class EchoServer(tmpDir: os.Path, locks: Locks) - extends Server[Option[Int]](tmpDir, () => (), 1000, locks) with Runnable{ + extends Server[Option[Int]](tmpDir, () => (), 1000, locks) with Runnable { def stateCache0 = None def main0( args: Array[String], @@ -41,8 +41,8 @@ class EchoServer(tmpDir: os.Path, locks: Locks) (true, None) } - def handleRun(clientSocket: java.net.Socket, - initialSystemProperties: Map[String,String]): Unit = ??? + def handleRun(clientSocket: java.net.Socket, initialSystemProperties: Map[String, String]): Unit = + ??? } object ClientServerTests extends TestSuite { diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index ac186959ed9..20c19a8e036 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -13,7 +13,7 @@ import mill.main.client.ProxyStream.Output import mill.main.client.lock.{Lock, Locks} import scala.util.Try -object MillServerMain{ +object MillServerMain { def main(args0: Array[String]): Unit = { // Disable SIGINT interrupt signal in the Mill server. // @@ -41,15 +41,18 @@ object MillServerMain{ } } class MillServerMain( - lockBase: os.Path, - interruptServer: () => Unit, - acceptTimeoutMillis: Int, - locks: Locks - ) - extends mill.main.server.Server[RunnerState](lockBase, interruptServer, acceptTimeoutMillis, locks) { + lockBase: os.Path, + interruptServer: () => Unit, + acceptTimeoutMillis: Int, + locks: Locks +) extends mill.main.server.Server[RunnerState]( + lockBase, + interruptServer, + acceptTimeoutMillis, + locks + ) { def stateCache0 = RunnerState.empty - def handleRun(clientSocket: Socket, initialSystemProperties: Map[String, String]): Unit = { val currentOutErr = clientSocket.getOutputStream @@ -88,18 +91,19 @@ class MillServerMain( () => try { val streams = new SystemStreams(stdout, stderr, proxiedSocketInput) - val (result, newStateCache) = try MillMain.main0( - args, - stateCache, - interactive, - streams, - None, - env.asScala.toMap, - idle = _, - userSpecifiedProperties.asScala.toMap, - initialSystemProperties - ) catch MillMain.handleMillException(streams.err, stateCache) - + val (result, newStateCache) = + try MillMain.main0( + args, + stateCache, + interactive, + streams, + None, + env.asScala.toMap, + idle = _, + userSpecifiedProperties.asScala.toMap, + initialSystemProperties + ) + catch MillMain.handleMillException(streams.err, stateCache) stateCache = newStateCache os.write.over( @@ -138,4 +142,4 @@ class MillServerMain( } finally ProxyStream.sendEnd(currentOutErr) // Send a termination } -} \ No newline at end of file +} From 11639228b38669aefa5eed40d68e7dac39901d0d Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 19:24:21 -0700 Subject: [PATCH 05/34] tweak --- main/server/src/mill/main/server/Server.scala | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index 65f689a5a35..c2657920fef 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -30,27 +30,23 @@ abstract class Server[T]( val initialSystemProperties = sys.props.toMap Server.tryLockBlock(locks.processLock) { - watchServerIdFile() - var running = true - while (running) { - + while ({ serverLog("listening on socket") val serverSocket = bindSocket() - val sockOpt = interruptWithTimeout(() => serverSocket.close(), () => serverSocket.accept()) - - sockOpt match { - case None => running = false + try interruptWithTimeout(() => serverSocket.close(), () => serverSocket.accept()) match { + case None => false case Some(sock) => - try { - serverLog("handling run") - try handleRun(sock, initialSystemProperties) - catch { case e: Throwable => serverLog(e + "\n" + e.getStackTrace.mkString("\n")) } - finally sock.close(); - } finally serverSocket.close() + serverLog("handling run") + try handleRun(sock, initialSystemProperties) + catch { case e: Throwable => serverLog(e + "\n" + e.getStackTrace.mkString("\n")) } + finally sock.close(); + true } - } + finally serverSocket.close() + }) () + }.getOrElse(throw new Exception("Mill server process already present, exiting")) } From 6c3b4527b6a302400b45ee7898f9bc90b8daaf85 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 19:26:16 -0700 Subject: [PATCH 06/34] . --- main/server/src/mill/main/server/Server.scala | 2 -- runner/src/mill/runner/MillServerMain.scala | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index c2657920fef..fe067ecf708 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -20,8 +20,6 @@ abstract class Server[T]( acceptTimeoutMillis: Int, locks: Locks ) { - def stateCache0: T - var stateCache = stateCache0 val originalStdout = System.out val serverId = scala.util.Random.nextLong().toString diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index 20c19a8e036..1a7e5003d62 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -51,7 +51,8 @@ class MillServerMain( acceptTimeoutMillis, locks ) { - def stateCache0 = RunnerState.empty + + var stateCache = RunnerState.empty def handleRun(clientSocket: Socket, initialSystemProperties: Map[String, String]): Unit = { From b43b0ea3e411205b73b65f6f2429bf2a421c2cde Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 19:26:25 -0700 Subject: [PATCH 07/34] . --- main/server/src/mill/main/server/Server.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index fe067ecf708..fe854fed88a 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -20,10 +20,9 @@ abstract class Server[T]( acceptTimeoutMillis: Int, locks: Locks ) { - - val originalStdout = System.out val serverId = scala.util.Random.nextLong().toString def serverLog(s: String) = os.write.append(serverDir / ServerFiles.serverLog, s + "\n") + def run(): Unit = { val initialSystemProperties = sys.props.toMap From 8f68c0bb9e4a8a1fad52e366461bc8c5367e401e Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 19:31:22 -0700 Subject: [PATCH 08/34] . --- main/server/src/mill/main/server/Server.scala | 102 +++++++++++++++- .../mill/main/server/ClientServerTests.scala | 3 - runner/src/mill/runner/MillServerMain.scala | 114 ++++-------------- 3 files changed, 124 insertions(+), 95 deletions(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index fe854fed88a..75e83ec17e2 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -20,6 +20,9 @@ abstract class Server[T]( acceptTimeoutMillis: Int, locks: Locks ) { + + var stateCache = stateCache0 + def stateCache0: T val serverId = scala.util.Random.nextLong().toString def serverLog(s: String) = os.write.append(serverDir / ServerFiles.serverLog, s + "\n") @@ -67,8 +70,6 @@ abstract class Server[T]( pipedInput } - def handleRun(clientSocket: Socket, initialSystemProperties: Map[String, String]): Unit - def watchServerIdFile() = { os.write.over(serverDir / ServerFiles.serverId, serverId) val serverIdThread = new Thread( @@ -122,6 +123,103 @@ abstract class Server[T]( interrupt = false } } + + def handleRun(clientSocket: Socket, initialSystemProperties: Map[String, String]): Unit = { + + val currentOutErr = clientSocket.getOutputStream + try { + val stdout = new PrintStream(new Output(currentOutErr, ProxyStream.OUT), true) + val stderr = new PrintStream(new Output(currentOutErr, ProxyStream.ERR), true) + + // Proxy the input stream through a pair of Piped**putStream via a pumper, + // as the `UnixDomainSocketInputStream` we get directly from the socket does + // not properly implement `available(): Int` and thus messes up polling logic + // that relies on that method + val proxiedSocketInput = proxyInputStreamThroughPumper(clientSocket.getInputStream) + + val argStream = os.read.inputStream(serverDir / ServerFiles.runArgs) + val interactive = argStream.read() != 0 + val clientMillVersion = Util.readString(argStream) + val serverMillVersion = BuildInfo.millVersion + if (clientMillVersion != serverMillVersion) { + stderr.println( + s"Mill version changed ($serverMillVersion -> $clientMillVersion), re-starting server" + ) + os.write( + serverDir / ServerFiles.exitCode, + Util.ExitServerCodeWhenVersionMismatch().toString.getBytes() + ) + System.exit(Util.ExitServerCodeWhenVersionMismatch()) + } + val args = Util.parseArgs(argStream) + val env = Util.parseMap(argStream) + val userSpecifiedProperties = Util.parseMap(argStream) + argStream.close() + + @volatile var done = false + @volatile var idle = false + val t = new Thread( + () => + try { + val (result, newStateCache) = main0( + args, + stateCache, + interactive, + new SystemStreams(stdout, stderr, proxiedSocketInput), + env.asScala.toMap, + idle = _, + userSpecifiedProperties.asScala.toMap, + initialSystemProperties + ) + + stateCache = newStateCache + os.write.over( + serverDir / ServerFiles.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() + // Try to give thread a moment to stop before we kill it for real + Thread.sleep(5) + try t.stop() + catch { + case e: UnsupportedOperationException => + // nothing we can do about, removed in Java 20 + case e: java.lang.Error if e.getMessage.contains("Cleaner terminated abnormally") => + // ignore this error and do nothing; seems benign + } + + // flush before closing the socket + System.out.flush() + System.err.flush() + + } finally ProxyStream.sendEnd(currentOutErr) // Send a termination + } + + def main0( + args: Array[String], + stateCache: T, + mainInteractive: Boolean, + streams: SystemStreams, + env: Map[String, String], + setIdle: Boolean => Unit, + userSpecifiedProperties: Map[String, String], + initialSystemProperties: Map[String, String] + ): (Boolean, T) + } object Server { diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 39dee39d058..e37c5182c32 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -40,9 +40,6 @@ class EchoServer(tmpDir: os.Path, locks: Locks) streams.err.flush() (true, None) } - - def handleRun(clientSocket: java.net.Socket, initialSystemProperties: Map[String, String]): Unit = - ??? } object ClientServerTests extends TestSuite { diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index 1a7e5003d62..0df6fcff324 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -52,95 +52,29 @@ class MillServerMain( locks ) { - var stateCache = RunnerState.empty - - def handleRun(clientSocket: Socket, initialSystemProperties: Map[String, String]): Unit = { - - val currentOutErr = clientSocket.getOutputStream - try { - val stdout = new PrintStream(new Output(currentOutErr, ProxyStream.OUT), true) - val stderr = new PrintStream(new Output(currentOutErr, ProxyStream.ERR), true) - - // Proxy the input stream through a pair of Piped**putStream via a pumper, - // as the `UnixDomainSocketInputStream` we get directly from the socket does - // not properly implement `available(): Int` and thus messes up polling logic - // that relies on that method - val proxiedSocketInput = proxyInputStreamThroughPumper(clientSocket.getInputStream) - - val argStream = os.read.inputStream(lockBase / ServerFiles.runArgs) - val interactive = argStream.read() != 0 - val clientMillVersion = Util.readString(argStream) - val serverMillVersion = BuildInfo.millVersion - if (clientMillVersion != serverMillVersion) { - stderr.println( - s"Mill version changed ($serverMillVersion -> $clientMillVersion), re-starting server" - ) - os.write( - lockBase / ServerFiles.exitCode, - Util.ExitServerCodeWhenVersionMismatch().toString.getBytes() - ) - System.exit(Util.ExitServerCodeWhenVersionMismatch()) - } - val args = Util.parseArgs(argStream) - val env = Util.parseMap(argStream) - val userSpecifiedProperties = Util.parseMap(argStream) - argStream.close() - - @volatile var done = false - @volatile var idle = false - val t = new Thread( - () => - try { - val streams = new SystemStreams(stdout, stderr, proxiedSocketInput) - val (result, newStateCache) = - try MillMain.main0( - args, - stateCache, - interactive, - streams, - None, - env.asScala.toMap, - idle = _, - userSpecifiedProperties.asScala.toMap, - initialSystemProperties - ) - catch MillMain.handleMillException(streams.err, stateCache) - - stateCache = newStateCache - os.write.over( - lockBase / ServerFiles.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() - // Try to give thread a moment to stop before we kill it for real - Thread.sleep(5) - try t.stop() - catch { - case e: UnsupportedOperationException => - // nothing we can do about, removed in Java 20 - case e: java.lang.Error if e.getMessage.contains("Cleaner terminated abnormally") => - // ignore this error and do nothing; seems benign - } - - // flush before closing the socket - System.out.flush() - System.err.flush() - - } finally ProxyStream.sendEnd(currentOutErr) // Send a termination + def stateCache0 = RunnerState.empty + + def main0( + args: Array[String], + stateCache: RunnerState, + mainInteractive: Boolean, + streams: SystemStreams, + env: Map[String, String], + setIdle: Boolean => Unit, + userSpecifiedProperties: Map[String, String], + initialSystemProperties: Map[String, String] + ): (Boolean, RunnerState) = { + try MillMain.main0( + args = args, + stateCache = stateCache, + mainInteractive = mainInteractive, + streams0 = streams, + bspLog = None, + env = env, + setIdle = setIdle, + userSpecifiedProperties0 = userSpecifiedProperties, + initialSystemProperties = initialSystemProperties + ) + catch MillMain.handleMillException(streams.err, stateCache) } - } From ec73354aab10dd312f203c55c0b27427a9cda9fb Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 19:47:16 -0700 Subject: [PATCH 09/34] . --- .../src/mill/main/client/MillClientMain.java | 15 +++++-- .../src/mill/main/client/MillLauncher.java | 14 +++--- .../mill/main/client/MillServerLauncher.java | 44 ++++++++++--------- .../src/mill/main/client/lock/Locks.java | 6 +-- main/server/src/mill/main/server/Server.scala | 40 ++++++++++------- runner/src/mill/runner/MillServerMain.scala | 44 +++++++++---------- 6 files changed, 89 insertions(+), 74 deletions(-) diff --git a/main/client/src/mill/main/client/MillClientMain.java b/main/client/src/mill/main/client/MillClientMain.java index ea0d2965602..19ffa2b21ec 100644 --- a/main/client/src/mill/main/client/MillClientMain.java +++ b/main/client/src/mill/main/client/MillClientMain.java @@ -1,8 +1,7 @@ package mill.main.client; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; - +import java.util.function.BiConsumer; /** * This is a Java implementation to speed up repetitive starts. * A Scala implementation would result in the JVM loading much more classes almost doubling the start-up times. @@ -33,9 +32,9 @@ public static void main(String[] args) throws Exception { MillNoServerLauncher.runMain(args); } else try { // start in client-server mode - int exitCode = MillServerLauncher.runMain(args); + int exitCode = MillServerLauncher.runMain(args, initServer); if (exitCode == Util.ExitServerCodeWhenVersionMismatch()) { - exitCode = MillServerLauncher.runMain(args); + exitCode = MillServerLauncher.runMain(args, initServer); } System.exit(exitCode); } catch (MillServerCouldNotBeStarted e) { @@ -53,4 +52,12 @@ public static void main(String[] args) throws Exception { } } } + + private static BiConsumer initServer = (serverDir, setJnaNoSys) -> { + try { + MillLauncher.launchMillServer(serverDir, setJnaNoSys); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; } diff --git a/main/client/src/mill/main/client/MillLauncher.java b/main/client/src/mill/main/client/MillLauncher.java index 0364f1c9720..fce989c3619 100644 --- a/main/client/src/mill/main/client/MillLauncher.java +++ b/main/client/src/mill/main/client/MillLauncher.java @@ -23,27 +23,27 @@ static int launchMillNoServer(String[] args) throws Exception { return configureRunMillProcess(builder, out + "/" + millNoServer).waitFor(); } - static void launchMillServer(String lockBase, boolean setJnaNoSys) throws Exception { + static void launchMillServer(String serverDir, boolean setJnaNoSys) throws Exception { List l = new ArrayList<>(); l.addAll(millLaunchJvmCommand(setJnaNoSys)); l.add("mill.runner.MillServerMain"); - l.add(new File(lockBase).getCanonicalPath()); + l.add(new File(serverDir).getCanonicalPath()); - File stdout = new java.io.File(lockBase + "/" + ServerFiles.stdout); - File stderr = new java.io.File(lockBase + "/" + ServerFiles.stderr); + File stdout = new java.io.File(serverDir + "/" + ServerFiles.stdout); + File stderr = new java.io.File(serverDir + "/" + ServerFiles.stderr); ProcessBuilder builder = new ProcessBuilder() .command(l) .redirectOutput(stdout) .redirectError(stderr); - configureRunMillProcess(builder, lockBase + "/" + ServerFiles.sandbox); + configureRunMillProcess(builder, serverDir + "/" + ServerFiles.sandbox); } static Process configureRunMillProcess(ProcessBuilder builder, - String lockBase) throws Exception { + String serverDir) throws Exception { builder.environment().put("MILL_WORKSPACE_ROOT", new File("").getCanonicalPath()); - File sandbox = new java.io.File(lockBase + "/" + ServerFiles.sandbox); + File sandbox = new java.io.File(serverDir + "/" + ServerFiles.sandbox); sandbox.mkdirs(); // builder.directory(sandbox); return builder.start(); diff --git a/main/client/src/mill/main/client/MillServerLauncher.java b/main/client/src/mill/main/client/MillServerLauncher.java index a788844e079..98b64176cf6 100644 --- a/main/client/src/mill/main/client/MillServerLauncher.java +++ b/main/client/src/mill/main/client/MillServerLauncher.java @@ -15,11 +15,12 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.Map; +import java.util.function.BiConsumer; public class MillServerLauncher { final static int tailerRefreshIntervalMillis = 2; final static int maxLockAttempts = 3; - public static int runMain(String[] args) throws Exception { + public static int runMain(String[] args, BiConsumer initServer) throws Exception { final boolean setJnaNoSys = System.getProperty("jna.nosys") == null; if (setJnaNoSys) { @@ -32,18 +33,24 @@ public static int runMain(String[] args) throws Exception { int serverIndex = 0; while (serverIndex < serverProcessesLimit) { // Try each possible server process (-1 to -5) serverIndex++; - final String lockBase = out + "/" + millWorker + versionAndJvmHomeEncoding + "-" + serverIndex; - java.io.File lockBaseFile = new java.io.File(lockBase); + final String serverDir = out + "/" + millWorker + versionAndJvmHomeEncoding + "-" + serverIndex; + java.io.File lockBaseFile = new java.io.File(serverDir); lockBaseFile.mkdirs(); int lockAttempts = 0; while (lockAttempts < maxLockAttempts) { // Try to lock a particular server try ( - Locks locks = Locks.files(lockBase); + Locks locks = Locks.files(serverDir); TryLocked clientLock = locks.clientLock.tryLock() ) { if (clientLock != null) { - return runMillServer(args, lockBase, setJnaNoSys, locks); + return runMillServer( + args, + serverDir, + setJnaNoSys, + locks, + () -> initServer.accept(serverDir, setJnaNoSys) + ); } } catch (Exception e) { for (File file : lockBaseFile.listFiles()) file.delete(); @@ -56,11 +63,12 @@ public static int runMain(String[] args) throws Exception { } static int runMillServer(String[] args, - String lockBase, + String serverDir, boolean setJnaNoSys, - Locks locks) throws Exception { - final File stdout = new java.io.File(lockBase + "/" + ServerFiles.stdout); - final File stderr = new java.io.File(lockBase + "/" + ServerFiles.stderr); + Locks locks, + Runnable initServer) throws Exception { + final File stdout = new java.io.File(serverDir + "/" + ServerFiles.stdout); + final File stderr = new java.io.File(serverDir + "/" + ServerFiles.stderr); try( final FileToStreamTailer stdoutTailer = new FileToStreamTailer(stdout, System.out, tailerRefreshIntervalMillis); @@ -69,14 +77,8 @@ static int runMillServer(String[] args, stdoutTailer.start(); stderrTailer.start(); final int exitCode = run( - lockBase, - () -> { - try { - MillLauncher.launchMillServer(lockBase, setJnaNoSys); - } catch (Exception e) { - throw new RuntimeException(e); - } - }, + serverDir, + initServer, locks, System.in, System.out, @@ -110,7 +112,7 @@ private static int getServerProcessesLimit(String jvmHomeEncoding) { return processLimit; } public static int run( - String lockBase, + String serverDir, Runnable initServer, Locks locks, InputStream stdin, @@ -119,7 +121,7 @@ public static int run( String[] args, Map env) throws Exception { - try (FileOutputStream f = new FileOutputStream(lockBase + "/" + ServerFiles.runArgs)) { + try (FileOutputStream f = new FileOutputStream(serverDir + "/" + ServerFiles.runArgs)) { f.write(System.console() != null ? 1 : 0); Util.writeString(f, BuildInfo.millVersion); Util.writeArgs(args, f); @@ -130,7 +132,7 @@ public static int run( while (locks.processLock.probe()) Thread.sleep(3); - String socketName = ServerFiles.pipe(lockBase); + String socketName = ServerFiles.pipe(serverDir); AFUNIXSocketAddress addr = AFUNIXSocketAddress.of(new File(socketName)); long retryStart = System.currentTimeMillis(); @@ -163,7 +165,7 @@ public static int run( outPumperThread.join(); try { - return Integer.parseInt(Files.readAllLines(Paths.get(lockBase + "/" + ServerFiles.exitCode)).get(0)); + return Integer.parseInt(Files.readAllLines(Paths.get(serverDir + "/" + ServerFiles.exitCode)).get(0)); } catch (Throwable e) { return Util.ExitClientCodeCannotReadFromExitCodeFile(); } finally { diff --git a/main/client/src/mill/main/client/lock/Locks.java b/main/client/src/mill/main/client/lock/Locks.java index 86f55ed8496..31cc5328288 100644 --- a/main/client/src/mill/main/client/lock/Locks.java +++ b/main/client/src/mill/main/client/lock/Locks.java @@ -35,10 +35,10 @@ public Locks(Lock clientLock, Lock processLock){ this.processLock = processLock; } - public static Locks files(String lockBase) throws Exception { + public static Locks files(String serverDir) throws Exception { return new Locks( - new FileLock(lockBase + "/" + ServerFiles.clientLock), - new FileLock(lockBase + "/" + ServerFiles.processLock) + new FileLock(serverDir + "/" + ServerFiles.clientLock), + new FileLock(serverDir + "/" + ServerFiles.processLock) ); } diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index 75e83ec17e2..db9e4b2f2f4 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -14,6 +14,12 @@ import mill.main.client.lock.{Lock, Locks} import scala.util.Try +/** + * Models a long-lived server that receives requests from a client and calls a [[main0]] + * method to run the commands in-process. Provides the command args, env variables, + * JVM properties, wrapped input/output streams, and other metadata related to the + * client command + */ abstract class Server[T]( serverDir: os.Path, interruptServer: () => Unit, @@ -36,14 +42,14 @@ abstract class Server[T]( serverLog("listening on socket") val serverSocket = bindSocket() try interruptWithTimeout(() => serverSocket.close(), () => serverSocket.accept()) match { - case None => false - case Some(sock) => - serverLog("handling run") - try handleRun(sock, initialSystemProperties) - catch { case e: Throwable => serverLog(e + "\n" + e.getStackTrace.mkString("\n")) } - finally sock.close(); - true - } + case None => false + case Some(sock) => + serverLog("handling run") + try handleRun(sock, initialSystemProperties) + catch { case e: Throwable => serverLog(e + "\n" + e.getStackTrace.mkString("\n")) } + finally sock.close(); + true + } finally serverSocket.close() }) () @@ -210,15 +216,15 @@ abstract class Server[T]( } def main0( - args: Array[String], - stateCache: T, - mainInteractive: Boolean, - streams: SystemStreams, - env: Map[String, String], - setIdle: Boolean => Unit, - userSpecifiedProperties: Map[String, String], - initialSystemProperties: Map[String, String] - ): (Boolean, T) + args: Array[String], + stateCache: T, + mainInteractive: Boolean, + streams: SystemStreams, + env: Map[String, String], + setIdle: Boolean => Unit, + userSpecifiedProperties: Map[String, String], + initialSystemProperties: Map[String, String] + ): (Boolean, T) } diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index 0df6fcff324..2ffb0ce1520 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -33,7 +33,7 @@ object MillServerMain { Try(System.getProperty("mill.server_timeout").toInt).getOrElse(5 * 60 * 1000) // 5 minutes new MillServerMain( - lockBase = os.Path(args0(0)), + serverDir = os.Path(args0(0)), () => System.exit(Util.ExitServerCodeWhenIdle()), acceptTimeoutMillis = acceptTimeoutMillis, Locks.files(args0(0)) @@ -41,12 +41,12 @@ object MillServerMain { } } class MillServerMain( - lockBase: os.Path, + serverDir: os.Path, interruptServer: () => Unit, acceptTimeoutMillis: Int, locks: Locks ) extends mill.main.server.Server[RunnerState]( - lockBase, + serverDir, interruptServer, acceptTimeoutMillis, locks @@ -55,26 +55,26 @@ class MillServerMain( def stateCache0 = RunnerState.empty def main0( - args: Array[String], - stateCache: RunnerState, - mainInteractive: Boolean, - streams: SystemStreams, - env: Map[String, String], - setIdle: Boolean => Unit, - userSpecifiedProperties: Map[String, String], - initialSystemProperties: Map[String, String] - ): (Boolean, RunnerState) = { + args: Array[String], + stateCache: RunnerState, + mainInteractive: Boolean, + streams: SystemStreams, + env: Map[String, String], + setIdle: Boolean => Unit, + userSpecifiedProperties: Map[String, String], + initialSystemProperties: Map[String, String] + ): (Boolean, RunnerState) = { try MillMain.main0( - args = args, - stateCache = stateCache, - mainInteractive = mainInteractive, - streams0 = streams, - bspLog = None, - env = env, - setIdle = setIdle, - userSpecifiedProperties0 = userSpecifiedProperties, - initialSystemProperties = initialSystemProperties - ) + args = args, + stateCache = stateCache, + mainInteractive = mainInteractive, + streams0 = streams, + bspLog = None, + env = env, + setIdle = setIdle, + userSpecifiedProperties0 = userSpecifiedProperties, + initialSystemProperties = initialSystemProperties + ) catch MillMain.handleMillException(streams.err, stateCache) } } From 1e695cb30497bea80856219ecb4758f401e627d1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 19:52:21 -0700 Subject: [PATCH 10/34] . --- main/client/src/mill/main/client/MillClientMain.java | 5 +---- main/client/src/mill/main/client/MillNoServerLauncher.java | 2 +- .../client/{MillLauncher.java => MillProcessLauncher.java} | 2 +- main/client/test/src/mill/main/client/MillEnvTests.java | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) rename main/client/src/mill/main/client/{MillLauncher.java => MillProcessLauncher.java} (99%) diff --git a/main/client/src/mill/main/client/MillClientMain.java b/main/client/src/mill/main/client/MillClientMain.java index 19ffa2b21ec..e0be8bed0de 100644 --- a/main/client/src/mill/main/client/MillClientMain.java +++ b/main/client/src/mill/main/client/MillClientMain.java @@ -7,9 +7,6 @@ * A Scala implementation would result in the JVM loading much more classes almost doubling the start-up times. */ public class MillClientMain { - - - public static void main(String[] args) throws Exception { boolean runNoServer = false; if (args.length > 0) { @@ -55,7 +52,7 @@ public static void main(String[] args) throws Exception { private static BiConsumer initServer = (serverDir, setJnaNoSys) -> { try { - MillLauncher.launchMillServer(serverDir, setJnaNoSys); + MillProcessLauncher.launchMillServer(serverDir, setJnaNoSys); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/main/client/src/mill/main/client/MillNoServerLauncher.java b/main/client/src/mill/main/client/MillNoServerLauncher.java index 99d69e3630e..ab434ecd673 100644 --- a/main/client/src/mill/main/client/MillNoServerLauncher.java +++ b/main/client/src/mill/main/client/MillNoServerLauncher.java @@ -44,7 +44,7 @@ public static LoadResult load() { public static void runMain(String[] args) throws Exception { LoadResult loadResult = load(); if (loadResult.millMainMethod.isPresent()) { - int exitVal = MillLauncher.launchMillNoServer(args); + int exitVal = MillProcessLauncher.launchMillNoServer(args); System.exit(exitVal); } else { throw new RuntimeException("Cannot load mill.runner.MillMain class"); diff --git a/main/client/src/mill/main/client/MillLauncher.java b/main/client/src/mill/main/client/MillProcessLauncher.java similarity index 99% rename from main/client/src/mill/main/client/MillLauncher.java rename to main/client/src/mill/main/client/MillProcessLauncher.java index fce989c3619..d97980163bc 100644 --- a/main/client/src/mill/main/client/MillLauncher.java +++ b/main/client/src/mill/main/client/MillProcessLauncher.java @@ -7,7 +7,7 @@ import java.io.IOException; import java.util.*; -public class MillLauncher { +public class MillProcessLauncher { static int launchMillNoServer(String[] args) throws Exception { boolean setJnaNoSys = System.getProperty("jna.nosys") == null; diff --git a/main/client/test/src/mill/main/client/MillEnvTests.java b/main/client/test/src/mill/main/client/MillEnvTests.java index f4af16e3966..af9884dea31 100644 --- a/main/client/test/src/mill/main/client/MillEnvTests.java +++ b/main/client/test/src/mill/main/client/MillEnvTests.java @@ -17,7 +17,7 @@ protected void initTests() { File file = new File( getClass().getClassLoader().getResource("file-wo-final-newline.txt").toURI() ); - List lines = MillLauncher.readOptsFileLines(file); + List lines = MillProcessLauncher.readOptsFileLines(file); expectEquals( lines, Arrays.asList( From 9263d228097f8104786b3c92646a4669ce57a4ee Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 19:55:52 -0700 Subject: [PATCH 11/34] . --- .../src/mill/main/client/MillClientMain.java | 4 +-- ...erverLauncher.java => ServerLauncher.java} | 27 ++++++++++++++++++- .../src/mill/main/client/lock/Locks.java | 24 +---------------- .../mill/main/server/ClientServerTests.scala | 2 +- 4 files changed, 30 insertions(+), 27 deletions(-) rename main/client/src/mill/main/client/{MillServerLauncher.java => ServerLauncher.java} (85%) diff --git a/main/client/src/mill/main/client/MillClientMain.java b/main/client/src/mill/main/client/MillClientMain.java index e0be8bed0de..a5fc14a6374 100644 --- a/main/client/src/mill/main/client/MillClientMain.java +++ b/main/client/src/mill/main/client/MillClientMain.java @@ -29,9 +29,9 @@ public static void main(String[] args) throws Exception { MillNoServerLauncher.runMain(args); } else try { // start in client-server mode - int exitCode = MillServerLauncher.runMain(args, initServer); + int exitCode = ServerLauncher.runMain(args, initServer); if (exitCode == Util.ExitServerCodeWhenVersionMismatch()) { - exitCode = MillServerLauncher.runMain(args, initServer); + exitCode = ServerLauncher.runMain(args, initServer); } System.exit(exitCode); } catch (MillServerCouldNotBeStarted e) { diff --git a/main/client/src/mill/main/client/MillServerLauncher.java b/main/client/src/mill/main/client/ServerLauncher.java similarity index 85% rename from main/client/src/mill/main/client/MillServerLauncher.java rename to main/client/src/mill/main/client/ServerLauncher.java index 98b64176cf6..940314cb063 100644 --- a/main/client/src/mill/main/client/MillServerLauncher.java +++ b/main/client/src/mill/main/client/ServerLauncher.java @@ -17,7 +17,32 @@ import java.util.Map; import java.util.function.BiConsumer; -public class MillServerLauncher { +/** + * Client side code that interacts with `Server.scala` in order to launch a generic + * long lived background server. + * + * The protocol is as follows: + * + * - Client: + * - Take clientLock + * - If processLock is not yet taken, it means server is not running, so spawn a server + * - Wait for server socket to be available for connection + * - Server: + * - Take processLock. + * - If already taken, it means another server was running + * (e.g. spawned by a different client) so exit immediately + * - Server: loop: + * - Listen for incoming client requests on serverSocket + * - Execute client request + * - If clientLock is released during execution, terminate server (otherwise + * we have no safe way of termianting the in-process request, so the server + * may continue running for arbitrarily long with no client attached) + * - Send `ProxyStream.END` packet and call `clientSocket.close()` + * - Client: + * - Wait for `ProxyStream.END` packet or `clientSocket.close()`, + * indicating server has finished execution and all data has been received + */ +public class ServerLauncher { final static int tailerRefreshIntervalMillis = 2; final static int maxLockAttempts = 3; public static int runMain(String[] args, BiConsumer initServer) throws Exception { diff --git a/main/client/src/mill/main/client/lock/Locks.java b/main/client/src/mill/main/client/lock/Locks.java index 31cc5328288..6de46cb0651 100644 --- a/main/client/src/mill/main/client/lock/Locks.java +++ b/main/client/src/mill/main/client/lock/Locks.java @@ -2,29 +2,7 @@ import mill.main.client.ServerFiles; -/** - * The locks used to manage the relationship of Mill between Mill's clients and servers. - * The protocol is as follows: - * - * - Client: - * - Take clientLock - * - If processLock is not yet taken, it means server is not running, so spawn a server - * - Wait for server socket to be available for connection - * - Server: - * - Take processLock. - * - If already taken, it means another server was running - * (e.g. spawned by a different client) so exit immediately - * - Server: loop: - * - Listen for incoming client requests on serverSocket - * - Execute client request - * - If clientLock is released during execution, terminate server (otherwise - * we have no safe way of termianting the in-process request, so the server - * may continue running for arbitrarily long with no client attached) - * - Send `ProxyStream.END` packet and call `clientSocket.close()` - * - Client: - * - Wait for `ProxyStream.END` packet or `clientSocket.close()`, - * indicating server has finished execution and all data has been received - */ + final public class Locks implements AutoCloseable { final public Lock clientLock; diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index e37c5182c32..5bd286072e9 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -69,7 +69,7 @@ object ClientServerTests extends TestSuite { )(env: Map[String, String], args: Array[String]) = { val (in, out, err) = initStreams() Server.lockBlock(locks.clientLock) { - mill.main.client.MillServerLauncher.run( + mill.main.client.ServerLauncher.run( tmpDir.toString, () => spawnEchoServer(tmpDir, locks), locks, From bb95b7d73e56a70b8cc80ed71f0debe696d182f7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 20:07:54 -0700 Subject: [PATCH 12/34] . --- build.sc | 9 +++-- .../client/MillServerCouldNotBeStarted.java | 7 ---- .../main/client/ServerCouldNotBeStarted.java | 7 ++++ .../src/mill/main/client/ServerLauncher.java | 2 +- main/client/src/mill/main/client/Util.java | 29 +++++++++++++++- .../src/mill/main/client/MillEnvTests.java | 2 +- .../mill/runner}/client/MillClientMain.java | 9 +++-- .../runner}/client/MillNoServerLauncher.java | 2 +- .../runner}/client/MillProcessLauncher.java | 33 ++++--------------- 9 files changed, 58 insertions(+), 42 deletions(-) delete mode 100644 main/client/src/mill/main/client/MillServerCouldNotBeStarted.java create mode 100644 main/client/src/mill/main/client/ServerCouldNotBeStarted.java rename {main/client/src/mill/main => runner/client/src/mill/runner}/client/MillClientMain.java (91%) rename {main/client/src/mill/main => runner/client/src/mill/runner}/client/MillNoServerLauncher.java (98%) rename {main/client/src/mill/main => runner/client/src/mill/runner}/client/MillProcessLauncher.java (86%) diff --git a/build.sc b/build.sc index d3a09a47af1..fd19d6796d7 100644 --- a/build.sc +++ b/build.sc @@ -1437,7 +1437,7 @@ def launcherScript( cmdClassPath: Agg[String] ) = { - val millMainClass = "mill.main.client.MillClientMain" + val millMainClass = "mill.runner.client.MillClientMain" Jvm.universalScript( shellCommands = { @@ -1548,8 +1548,13 @@ def launcherScript( } object runner extends MillPublishScalaModule { + object client extends MillPublishJavaModule{ + def buildInfoPackageName = "mill.runner.client" + def moduleDeps = Seq(main.client) + } + def moduleDeps = Seq( - scalalib, scalajslib, scalanativelib, bsp, linenumbers, main.codesig, main.server + scalalib, scalajslib, scalanativelib, bsp, linenumbers, main.codesig, main.server, client ) def skipPreviousVersions: T[Seq[String]] = Seq("0.11.0-M7") diff --git a/main/client/src/mill/main/client/MillServerCouldNotBeStarted.java b/main/client/src/mill/main/client/MillServerCouldNotBeStarted.java deleted file mode 100644 index b5b3bcfb6bb..00000000000 --- a/main/client/src/mill/main/client/MillServerCouldNotBeStarted.java +++ /dev/null @@ -1,7 +0,0 @@ -package mill.main.client; - -public class MillServerCouldNotBeStarted extends Exception { - public MillServerCouldNotBeStarted(String msg) { - super(msg); - } -} diff --git a/main/client/src/mill/main/client/ServerCouldNotBeStarted.java b/main/client/src/mill/main/client/ServerCouldNotBeStarted.java new file mode 100644 index 00000000000..908dbce62f3 --- /dev/null +++ b/main/client/src/mill/main/client/ServerCouldNotBeStarted.java @@ -0,0 +1,7 @@ +package mill.main.client; + +public class ServerCouldNotBeStarted extends Exception { + public ServerCouldNotBeStarted(String msg) { + super(msg); + } +} diff --git a/main/client/src/mill/main/client/ServerLauncher.java b/main/client/src/mill/main/client/ServerLauncher.java index 940314cb063..e8a6bbd038c 100644 --- a/main/client/src/mill/main/client/ServerLauncher.java +++ b/main/client/src/mill/main/client/ServerLauncher.java @@ -84,7 +84,7 @@ public static int runMain(String[] args, BiConsumer initServer) } } } - throw new MillServerCouldNotBeStarted("Reached max server processes limit: " + serverProcessesLimit); + throw new ServerCouldNotBeStarted("Reached max server processes limit: " + serverProcessesLimit); } static int runMillServer(String[] args, diff --git a/main/client/src/mill/main/client/Util.java b/main/client/src/mill/main/client/Util.java index 2dcb69c514f..c432d70df21 100644 --- a/main/client/src/mill/main/client/Util.java +++ b/main/client/src/mill/main/client/Util.java @@ -3,7 +3,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; +import java.io.File; +import java.io.FileNotFoundException; import java.math.BigInteger; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -12,6 +13,9 @@ import java.util.Base64; import java.util.HashMap; import java.util.Map; +import java.util.List; +import java.util.LinkedList; +import java.util.Scanner; public class Util { // use methods instead of constants to avoid inlining by compiler @@ -128,4 +132,27 @@ static String sha1Hash(String path) throws NoSuchAlgorithmException { return Base64.getEncoder().encodeToString(digest); } + /** + * Reads a file, ignoring empty or comment lines + * + * @return The non-empty lines of the files or an empty list, if the file does not exists + */ + public static List readOptsFileLines(final File file) { + final List vmOptions = new LinkedList<>(); + try ( + final Scanner sc = new Scanner(file) + ) { + while (sc.hasNextLine()) { + String arg = sc.nextLine(); + String trimmed = arg.trim(); + if (!trimmed.isEmpty() && !trimmed.startsWith("#")) { + vmOptions.add(arg); + } + } + } catch (FileNotFoundException e) { + // ignored + } + return vmOptions; + } + } diff --git a/main/client/test/src/mill/main/client/MillEnvTests.java b/main/client/test/src/mill/main/client/MillEnvTests.java index af9884dea31..bcb19366142 100644 --- a/main/client/test/src/mill/main/client/MillEnvTests.java +++ b/main/client/test/src/mill/main/client/MillEnvTests.java @@ -17,7 +17,7 @@ protected void initTests() { File file = new File( getClass().getClassLoader().getResource("file-wo-final-newline.txt").toURI() ); - List lines = MillProcessLauncher.readOptsFileLines(file); + List lines = Util.readOptsFileLines(file); expectEquals( lines, Arrays.asList( diff --git a/main/client/src/mill/main/client/MillClientMain.java b/runner/client/src/mill/runner/client/MillClientMain.java similarity index 91% rename from main/client/src/mill/main/client/MillClientMain.java rename to runner/client/src/mill/runner/client/MillClientMain.java index a5fc14a6374..f9dcb9bc394 100644 --- a/main/client/src/mill/main/client/MillClientMain.java +++ b/runner/client/src/mill/runner/client/MillClientMain.java @@ -1,7 +1,12 @@ -package mill.main.client; +package mill.runner.client; import java.util.Arrays; import java.util.function.BiConsumer; +import mill.main.client.ServerLauncher; +import mill.main.client.ServerFiles; +import mill.main.client.Util; +import mill.main.client.ServerCouldNotBeStarted; + /** * This is a Java implementation to speed up repetitive starts. * A Scala implementation would result in the JVM loading much more classes almost doubling the start-up times. @@ -34,7 +39,7 @@ public static void main(String[] args) throws Exception { exitCode = ServerLauncher.runMain(args, initServer); } System.exit(exitCode); - } catch (MillServerCouldNotBeStarted e) { + } catch (ServerCouldNotBeStarted e) { // TODO: try to run in-process System.err.println("Could not start a Mill server process.\n" + "This could be caused by too many already running Mill instances " + diff --git a/main/client/src/mill/main/client/MillNoServerLauncher.java b/runner/client/src/mill/runner/client/MillNoServerLauncher.java similarity index 98% rename from main/client/src/mill/main/client/MillNoServerLauncher.java rename to runner/client/src/mill/runner/client/MillNoServerLauncher.java index ab434ecd673..b07655b8873 100644 --- a/main/client/src/mill/main/client/MillNoServerLauncher.java +++ b/runner/client/src/mill/runner/client/MillNoServerLauncher.java @@ -1,4 +1,4 @@ -package mill.main.client; +package mill.runner.client; import java.lang.reflect.Method; import java.util.Optional; diff --git a/main/client/src/mill/main/client/MillProcessLauncher.java b/runner/client/src/mill/runner/client/MillProcessLauncher.java similarity index 86% rename from main/client/src/mill/main/client/MillProcessLauncher.java rename to runner/client/src/mill/runner/client/MillProcessLauncher.java index d97980163bc..3245ee78a76 100644 --- a/main/client/src/mill/main/client/MillProcessLauncher.java +++ b/runner/client/src/mill/runner/client/MillProcessLauncher.java @@ -1,4 +1,4 @@ -package mill.main.client; +package mill.runner.client; import static mill.main.client.OutFiles.*; import java.io.File; @@ -6,6 +6,9 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.util.*; +import mill.main.client.Util; +import mill.main.client.ServerFiles; +import mill.main.client.ServerCouldNotBeStarted; public class MillProcessLauncher { @@ -146,7 +149,7 @@ static List millLaunchJvmCommand(boolean setJnaNoSys) { // extra opts File millJvmOptsFile = millJvmOptsFile(); if (millJvmOptsFile.exists()) { - vmOptions.addAll(readOptsFileLines(millJvmOptsFile)); + vmOptions.addAll(Util.readOptsFileLines(millJvmOptsFile)); } vmOptions.add("-cp"); @@ -156,30 +159,6 @@ static List millLaunchJvmCommand(boolean setJnaNoSys) { } static List readMillJvmOpts() { - return readOptsFileLines(millJvmOptsFile()); + return Util.readOptsFileLines(millJvmOptsFile()); } - - /** - * Reads a file, ignoring empty or comment lines - * - * @return The non-empty lines of the files or an empty list, if the file does not exists - */ - static List readOptsFileLines(final File file) { - final List vmOptions = new LinkedList<>(); - try ( - final Scanner sc = new Scanner(file) - ) { - while (sc.hasNextLine()) { - String arg = sc.nextLine(); - String trimmed = arg.trim(); - if (!trimmed.isEmpty() && !trimmed.startsWith("#")) { - vmOptions.add(arg); - } - } - } catch (FileNotFoundException e) { - // ignored - } - return vmOptions; - } - } From 76bd88e1ec350713eafb854e611135a565ea48d3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 20:36:48 -0700 Subject: [PATCH 13/34] . --- main/client/src/mill/main/client/InputPumper.java | 7 ++++++- main/server/src/mill/main/server/Server.scala | 9 +++++---- .../test/src/mill/main/server/ClientServerTests.scala | 4 +++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/main/client/src/mill/main/client/InputPumper.java b/main/client/src/mill/main/client/InputPumper.java index cbd5ecf460d..1bddc0bc656 100644 --- a/main/client/src/mill/main/client/InputPumper.java +++ b/main/client/src/mill/main/client/InputPumper.java @@ -38,7 +38,12 @@ public void run() { } else if (checkAvailable && src.available() == 0) Thread.sleep(2); else { - int n = src.read(buffer); + int n; + try{ + n = src.read(buffer); + } catch (Exception e){ + n = -1; + } if (n == -1) { running = false; } diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index db9e4b2f2f4..036a15af37a 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -29,8 +29,9 @@ abstract class Server[T]( var stateCache = stateCache0 def stateCache0: T - val serverId = scala.util.Random.nextLong().toString - def serverLog(s: String) = os.write.append(serverDir / ServerFiles.serverLog, s + "\n") + + val serverId = java.lang.Long.toHexString(scala.util.Random.nextLong()) + def serverLog(s: String) = println(serverDir / ServerFiles.serverLog, s + "\n") def run(): Unit = { val initialSystemProperties = sys.props.toMap @@ -85,11 +86,11 @@ abstract class Server[T]( Try(os.read(serverDir / ServerFiles.serverId)).toOption match { case None => serverLog("serverId file missing, exiting") - System.exit(0) + interruptServer() case Some(s) => if (s != serverId) { serverLog(s"serverId file contents $s does not match serverId $serverId, exiting") - System.exit(0) + interruptServer() } } } diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 5bd286072e9..93fc9887080 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -11,6 +11,7 @@ import utest._ class EchoServer(tmpDir: os.Path, locks: Locks) extends Server[Option[Int]](tmpDir, () => (), 1000, locks) with Runnable { def stateCache0 = None + override def serverLog(s: String) = println(serverId + " " + s) def main0( args: Array[String], stateCache: Option[Int], @@ -53,7 +54,8 @@ object ClientServerTests extends TestSuite { (in, out, err) } def init() = { - val tmpDir = os.temp.dir() + val tmpDir = os.temp.dir(os.pwd) + println("tmpDir " + tmpDir) val locks = Locks.memory() (tmpDir, locks) From f9e252fae524faea985862399a0facc8ba3f3582 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 20:38:40 -0700 Subject: [PATCH 14/34] . --- main/server/test/src/mill/main/server/ClientServerTests.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 93fc9887080..47e1c7e5dbe 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -81,7 +81,7 @@ object ClientServerTests extends TestSuite { args, env.asJava ) - Thread.sleep(200) + (new String(out.toByteArray), new String(err.toByteArray)) } } @@ -108,7 +108,6 @@ object ClientServerTests extends TestSuite { // Give a bit of time for the server to release the lock and // re-acquire it to signal to the client that it"s" done - Thread.sleep(100) assert( locks.clientLock.probe(), From aa465661d1505252e1ae61d0fc93b4170d8f4ae5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 20:38:50 -0700 Subject: [PATCH 15/34] . --- main/server/test/src/mill/main/server/ClientServerTests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 47e1c7e5dbe..e8d712146d4 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -55,7 +55,7 @@ object ClientServerTests extends TestSuite { } def init() = { val tmpDir = os.temp.dir(os.pwd) - println("tmpDir " + tmpDir) + val locks = Locks.memory() (tmpDir, locks) From ad9566bad9054a20306cfa737af744e5a660b380 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 20:39:04 -0700 Subject: [PATCH 16/34] . --- main/server/test/src/mill/main/server/ClientServerTests.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index e8d712146d4..6ec276cf06e 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -54,8 +54,8 @@ object ClientServerTests extends TestSuite { (in, out, err) } def init() = { - val tmpDir = os.temp.dir(os.pwd) - + val tmpDir = os.temp.dir() + val locks = Locks.memory() (tmpDir, locks) From 4cd544bf83a5d6fe78b2abf587b72271dde67796 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 20:39:18 -0700 Subject: [PATCH 17/34] . --- main/server/test/src/mill/main/server/ClientServerTests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 6ec276cf06e..397301564e0 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -54,7 +54,7 @@ object ClientServerTests extends TestSuite { (in, out, err) } def init() = { - val tmpDir = os.temp.dir() + val tmpDir = os.temp.dir(os.pwd) val locks = Locks.memory() From 8db7efbc0686832b2c4519bc955f593852e93524 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 20:42:59 -0700 Subject: [PATCH 18/34] . --- .../test/src/mill/main/server/ClientServerTests.scala | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 397301564e0..a2f8a3fc9f5 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -54,7 +54,9 @@ object ClientServerTests extends TestSuite { (in, out, err) } def init() = { - val tmpDir = os.temp.dir(os.pwd) + val dest = os.pwd / "out" + os.makeDir.all(dest) + val tmpDir = os.temp.dir(dest) val locks = Locks.memory() @@ -170,10 +172,6 @@ object ClientServerTests extends TestSuite { err1 == "" ) - // Give a bit of time for the server to release the lock and - // re-acquire it to signal to the client that it's done - Thread.sleep(100) - assert( locks.clientLock.probe(), !locks.processLock.probe() From 2a6f641c5dc7962f7af40a85697108ab4b4ca658 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 20:50:05 -0700 Subject: [PATCH 19/34] refactor --- main/server/src/mill/main/server/Server.scala | 2 +- .../mill/main/server/ClientServerTests.scala | 202 +++++++++--------- runner/src/mill/runner/MillServerMain.scala | 4 +- 3 files changed, 105 insertions(+), 103 deletions(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index 036a15af37a..95812ae35a2 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -22,11 +22,11 @@ import scala.util.Try */ abstract class Server[T]( serverDir: os.Path, - interruptServer: () => Unit, acceptTimeoutMillis: Int, locks: Locks ) { + def interruptServer(): Unit var stateCache = stateCache0 def stateCache0: T diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index a2f8a3fc9f5..c537ff3dfdb 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -9,7 +9,9 @@ import scala.jdk.CollectionConverters._ import utest._ class EchoServer(tmpDir: os.Path, locks: Locks) - extends Server[Option[Int]](tmpDir, () => (), 1000, locks) with Runnable { + extends Server[Option[Int]](tmpDir, 1000, locks) with Runnable { + var running = true + def interruptServer() = running = false def stateCache0 = None override def serverLog(s: String) = println(serverId + " " + s) def main0( @@ -47,61 +49,65 @@ object ClientServerTests extends TestSuite { val ENDL = System.lineSeparator() - def initStreams() = { - val in = new ByteArrayInputStream(s"hello${ENDL}".getBytes()) - val out = new ByteArrayOutputStream() - val err = new ByteArrayOutputStream() - (in, out, err) - } - def init() = { + class Tester { + + val dest = os.pwd / "out" os.makeDir.all(dest) val tmpDir = os.temp.dir(dest) val locks = Locks.memory() - (tmpDir, locks) - } - def spawnEchoServer(tmpDir: os.Path, locks: Locks): Unit = { - new Thread(new EchoServer(tmpDir, locks)).start() - } + def initStreams() = { + val in = new ByteArrayInputStream(s"hello${ENDL}".getBytes()) + val out = new ByteArrayOutputStream() + val err = new ByteArrayOutputStream() + (in, out, err) + } - def runClientAux( - tmpDir: os.Path, - locks: Locks - )(env: Map[String, String], args: Array[String]) = { - val (in, out, err) = initStreams() - Server.lockBlock(locks.clientLock) { - mill.main.client.ServerLauncher.run( - tmpDir.toString, - () => spawnEchoServer(tmpDir, locks), - locks, - in, - out, - err, - args, - env.asJava - ) + def spawnEchoServer(tmpDir: os.Path, locks: Locks): Unit = { + new Thread(new EchoServer(tmpDir, locks)).start() + } + + def runClient(tmpDir: os.Path, + locks: Locks + )(env: Map[String, String], args: Array[String]) = { + val (in, out, err) = initStreams() + Server.lockBlock(locks.clientLock) { + mill.main.client.ServerLauncher.run( + tmpDir.toString, + () => spawnEchoServer(tmpDir, locks), + locks, + in, + out, + err, + args, + env.asJava + ) - (new String(out.toByteArray), new String(err.toByteArray)) + (new String(out.toByteArray), new String(err.toByteArray)) + } } + + def apply(env: Map[String, String], args: Array[String]) = runClient(tmpDir, locks)(env, args) + } def tests = Tests { "hello" - { - val (tmpDir, locks) = init() - def runClient(s: String) = runClientAux(tmpDir, locks)(Map.empty, Array(s)) + val tester = new Tester + // Make sure the simple "have the client start a server and // exchange one message" workflow works from end to end. assert( - locks.clientLock.probe(), - locks.processLock.probe() + tester.locks.clientLock.probe(), + tester.locks.processLock.probe() ) - val (out1, err1) = runClient("world") + val (out1, err1) = tester(Map(), Array("world")) assert( out1 == s"helloworld${ENDL}", @@ -112,12 +118,12 @@ object ClientServerTests extends TestSuite { // re-acquire it to signal to the client that it"s" done assert( - locks.clientLock.probe(), - !locks.processLock.probe() + tester.locks.clientLock.probe(), + !tester.locks.processLock.probe() ) // A seecond client in sequence connect to the same server - val (out2, err2) = runClient(" WORLD") + val (out2, err2) = tester(Map(), Array(" WORLD")) assert( out2 == s"hello WORLD${ENDL}", @@ -128,87 +134,85 @@ object ClientServerTests extends TestSuite { // Make sure the server times out of not used for a while Thread.sleep(2000) assert( - locks.clientLock.probe(), - locks.processLock.probe() + tester.locks.clientLock.probe(), + tester.locks.processLock.probe() ) // Have a third client spawn/connect-to a new server at the same path - val (out3, err3) = runClient(" World") + val (out3, err3) = tester(Map(), Array(" World")) assert( out3 == s"hello World${ENDL}", err3 == s"HELLO World${ENDL}" ) } + } - "envVars" - retry(3) { - val (tmpDir, locks) = init() - - def runClient(env: Map[String, String]) = runClientAux(tmpDir, locks)(env, Array()) + "envVars" - retry(3) { + val tester = new Tester - // Make sure the simple "have the client start a server and - // exchange one message" workflow works from end to end. + // Make sure the simple "have the client start a server and + // exchange one message" workflow works from end to end. - assert( - locks.clientLock.probe(), - locks.processLock.probe() - ) + assert( + tester.locks.clientLock.probe(), + tester.locks.processLock.probe() + ) - def longString(s: String) = Array.fill(1000)(s).mkString - val b1000 = longString("b") - val c1000 = longString("c") - val a1000 = longString("a") + def longString(s: String) = Array.fill(1000)(s).mkString + val b1000 = longString("b") + val c1000 = longString("c") + val a1000 = longString("a") - val env = Map( - "a" -> a1000, - "b" -> b1000, - "c" -> c1000 - ) + val env = Map( + "a" -> a1000, + "b" -> b1000, + "c" -> c1000 + ) - val (out1, err1) = runClient(env) - val expected = s"a=$a1000${ENDL}b=$b1000${ENDL}c=$c1000${ENDL}" + val (out1, err1) = tester(env, Array()) + val expected = s"a=$a1000${ENDL}b=$b1000${ENDL}c=$c1000${ENDL}" - assert( - out1 == expected, - err1 == "" - ) + assert( + out1 == expected, + err1 == "" + ) - assert( - locks.clientLock.probe(), - !locks.processLock.probe() - ) + assert( + tester.locks.clientLock.probe(), + !tester.locks.processLock.probe() + ) - val path = List( - "/Users/foo/Library/Haskell/bin", - "/usr/local/git/bin", - "/sw/bin/", - "/usr/local/bin", - "/usr/local/", - "/usr/local/sbin", - "/usr/local/mysql/bin", - "/usr/local/bin", - "/usr/bin", - "/bin", - "/usr/sbin", - "/sbin", - "/opt/X11/bin", - "/usr/local/MacGPG2/bin", - "/Library/TeX/texbin", - "/usr/local/bin/", - "/Users/foo/bin", - "/Users/foo/go/bin", - "~/.bloop" - ) + val path = List( + "/Users/foo/Library/Haskell/bin", + "/usr/local/git/bin", + "/sw/bin/", + "/usr/local/bin", + "/usr/local/", + "/usr/local/sbin", + "/usr/local/mysql/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + "/usr/sbin", + "/sbin", + "/opt/X11/bin", + "/usr/local/MacGPG2/bin", + "/Library/TeX/texbin", + "/usr/local/bin/", + "/Users/foo/bin", + "/Users/foo/go/bin", + "~/.bloop" + ) - val pathEnvVar = path.mkString(":") - val (out2, err2) = runClient(Map("PATH" -> pathEnvVar)) + val pathEnvVar = path.mkString(":") + val (out2, err2) = tester(Map("PATH" -> pathEnvVar), Array()) - val expected2 = s"PATH=$pathEnvVar${ENDL}" + val expected2 = s"PATH=$pathEnvVar${ENDL}" - assert( - out2 == expected2, - err2 == "" - ) - } + assert( + out2 == expected2, + err2 == "" + ) } } } diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index 2ffb0ce1520..4a1aaf75041 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -34,7 +34,6 @@ object MillServerMain { new MillServerMain( serverDir = os.Path(args0(0)), - () => System.exit(Util.ExitServerCodeWhenIdle()), acceptTimeoutMillis = acceptTimeoutMillis, Locks.files(args0(0)) ).run() @@ -42,16 +41,15 @@ object MillServerMain { } class MillServerMain( serverDir: os.Path, - interruptServer: () => Unit, acceptTimeoutMillis: Int, locks: Locks ) extends mill.main.server.Server[RunnerState]( serverDir, - interruptServer, acceptTimeoutMillis, locks ) { + def interruptServer() = System.exit(Util.ExitServerCodeWhenIdle()) def stateCache0 = RunnerState.empty def main0( From 7a2847d643151822a50aa922cace253663abf2d3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 21:10:20 -0700 Subject: [PATCH 20/34] . --- main/server/src/mill/main/server/Server.scala | 16 ++- .../mill/main/server/ClientServerTests.scala | 119 +++++++++++------- 2 files changed, 81 insertions(+), 54 deletions(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index 95812ae35a2..dcbce9be97a 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -34,9 +34,10 @@ abstract class Server[T]( def serverLog(s: String) = println(serverDir / ServerFiles.serverLog, s + "\n") def run(): Unit = { + serverLog("running server") val initialSystemProperties = sys.props.toMap - Server.tryLockBlock(locks.processLock) { + try Server.tryLockBlock(locks.processLock) { watchServerIdFile() while ({ @@ -55,6 +56,7 @@ abstract class Server[T]( }) () }.getOrElse(throw new Exception("Mill server process already present, exiting")) + finally serverLog("exiting server") } def bindSocket() = { @@ -80,21 +82,23 @@ abstract class Server[T]( def watchServerIdFile() = { os.write.over(serverDir / ServerFiles.serverId, serverId) val serverIdThread = new Thread( - () => { - while (true) { + () => + while ( { Thread.sleep(100) Try(os.read(serverDir / ServerFiles.serverId)).toOption match { case None => serverLog("serverId file missing, exiting") interruptServer() + false case Some(s) => - if (s != serverId) { + if (s != serverId) true + else { serverLog(s"serverId file contents $s does not match serverId $serverId, exiting") interruptServer() + false } } - } - }, + })() , "Server ID Checker Thread" ) serverIdThread.start() diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index c537ff3dfdb..b67356b7664 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -8,50 +8,53 @@ import mill.api.SystemStreams import scala.jdk.CollectionConverters._ import utest._ -class EchoServer(tmpDir: os.Path, locks: Locks) - extends Server[Option[Int]](tmpDir, 1000, locks) with Runnable { - var running = true - def interruptServer() = running = false - def stateCache0 = None - override def serverLog(s: String) = println(serverId + " " + s) - def main0( - args: Array[String], - stateCache: Option[Int], - mainInteractive: Boolean, - streams: SystemStreams, - env: Map[String, String], - setIdle: Boolean => Unit, - systemProperties: Map[String, String], - initialSystemProperties: Map[String, String] - ) = { - - val reader = new BufferedReader(new InputStreamReader(streams.in)) - val str = reader.readLine() - if (args.nonEmpty) { - streams.out.println(str + args(0)) - } - env.toSeq.sortBy(_._1).foreach { - case (key, value) => streams.out.println(s"$key=$value") - } - systemProperties.toSeq.sortBy(_._1).foreach { - case (key, value) => streams.out.println(s"$key=$value") - } - if (args.nonEmpty) { - streams.err.println(str.toUpperCase + args(0)) - } - streams.out.flush() - streams.err.flush() - (true, None) - } -} object ClientServerTests extends TestSuite { val ENDL = System.lineSeparator() + class EchoServer(override val serverId: String, log: String => Unit, tmpDir: os.Path, locks: Locks) + extends Server[Option[Int]](tmpDir, 1000, locks) with Runnable { + def interruptServer() = () + def stateCache0 = None - class Tester { + override def serverLog(s: String) = { + log(serverId + " " + s) + } + def main0( + args: Array[String], + stateCache: Option[Int], + mainInteractive: Boolean, + streams: SystemStreams, + env: Map[String, String], + setIdle: Boolean => Unit, + systemProperties: Map[String, String], + initialSystemProperties: Map[String, String] + ) = { + + val reader = new BufferedReader(new InputStreamReader(streams.in)) + val str = reader.readLine() + if (args.nonEmpty) { + streams.out.println(str + args(0)) + } + env.toSeq.sortBy(_._1).foreach { + case (key, value) => streams.out.println(s"$key=$value") + } + systemProperties.toSeq.sortBy(_._1).foreach { + case (key, value) => streams.out.println(s"$key=$value") + } + if (args.nonEmpty) { + streams.err.println(str.toUpperCase + args(0)) + } + streams.out.flush() + streams.err.flush() + (true, None) + } + } + class Tester { + var nextServerId: Int = 0 + val terminatedServers = collection.mutable.Set.empty[String] val dest = os.pwd / "out" os.makeDir.all(dest) val tmpDir = os.temp.dir(dest) @@ -67,17 +70,25 @@ object ClientServerTests extends TestSuite { } def spawnEchoServer(tmpDir: os.Path, locks: Locks): Unit = { - new Thread(new EchoServer(tmpDir, locks)).start() + val serverId = "server-" + nextServerId + nextServerId += 1 + new Thread(new EchoServer(serverId, log, tmpDir, locks)).start() + } + def log(s: String) = { + logs.append(s) + println(s) } + val logs = collection.mutable.Buffer.empty[String] - def runClient(tmpDir: os.Path, - locks: Locks + + def runClient(serverDir: os.Path, + locks: Locks )(env: Map[String, String], args: Array[String]) = { val (in, out, err) = initStreams() Server.lockBlock(locks.clientLock) { mill.main.client.ServerLauncher.run( - tmpDir.toString, - () => spawnEchoServer(tmpDir, locks), + serverDir.toString, + () => spawnEchoServer(serverDir, locks), locks, in, out, @@ -86,7 +97,7 @@ object ClientServerTests extends TestSuite { env.asJava ) - (new String(out.toByteArray), new String(err.toByteArray)) + (serverDir, new String(out.toByteArray), new String(err.toByteArray)) } } @@ -107,7 +118,7 @@ object ClientServerTests extends TestSuite { tester.locks.processLock.probe() ) - val (out1, err1) = tester(Map(), Array("world")) + val (_, out1, err1) = tester(Map(), Array("world")) assert( out1 == s"helloworld${ENDL}", @@ -123,7 +134,7 @@ object ClientServerTests extends TestSuite { ) // A seecond client in sequence connect to the same server - val (out2, err2) = tester(Map(), Array(" WORLD")) + val (_, out2, err2) = tester(Map(), Array(" WORLD")) assert( out2 == s"hello WORLD${ENDL}", @@ -138,12 +149,24 @@ object ClientServerTests extends TestSuite { tester.locks.processLock.probe() ) + val exitingServerLogs = tester.logs.collect{case s"$serverId exiting server" => serverId} + assert(exitingServerLogs == Seq("server-0")) + // Have a third client spawn/connect-to a new server at the same path - val (out3, err3) = tester(Map(), Array(" World")) + val (serverDir3, out3, err3) = tester(Map(), Array(" World")) assert( out3 == s"hello World${ENDL}", err3 == s"HELLO World${ENDL}" ) + + // Make sure if we delete the out dir, the server notices and exits + os.remove.all(serverDir3) + Thread.sleep(500) + + val missingServerIdLogs = + tester.logs.collect{case s"$serverId serverId file missing, exiting" => serverId} + assert(missingServerIdLogs == Seq("server-1")) + } } @@ -169,7 +192,7 @@ object ClientServerTests extends TestSuite { "c" -> c1000 ) - val (out1, err1) = tester(env, Array()) + val (_, out1, err1) = tester(env, Array()) val expected = s"a=$a1000${ENDL}b=$b1000${ENDL}c=$c1000${ENDL}" assert( @@ -205,7 +228,7 @@ object ClientServerTests extends TestSuite { ) val pathEnvVar = path.mkString(":") - val (out2, err2) = tester(Map("PATH" -> pathEnvVar), Array()) + val (_, out2, err2) = tester(Map("PATH" -> pathEnvVar), Array()) val expected2 = s"PATH=$pathEnvVar${ENDL}" From c4a3c54cc5bd05c40465667c8c652726e70162a7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 21:18:06 -0700 Subject: [PATCH 21/34] . --- main/server/src/mill/main/server/Server.scala | 18 +++++++++--------- .../mill/main/server/ClientServerTests.scala | 13 +++++++------ runner/src/mill/runner/MillServerMain.scala | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index dcbce9be97a..971cb0d6ca3 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -26,7 +26,7 @@ abstract class Server[T]( locks: Locks ) { - def interruptServer(): Unit + def exitServer(): Unit var stateCache = stateCache0 def stateCache0: T @@ -55,8 +55,8 @@ abstract class Server[T]( finally serverSocket.close() }) () - }.getOrElse(throw new Exception("Mill server process already present, exiting")) - finally serverLog("exiting server") + }.getOrElse(throw new Exception("Mill server process already present")) + finally exitServer() } def bindSocket() = { @@ -87,14 +87,14 @@ abstract class Server[T]( Thread.sleep(100) Try(os.read(serverDir / ServerFiles.serverId)).toOption match { case None => - serverLog("serverId file missing, exiting") - interruptServer() + serverLog("serverId file missing") + exitServer() false case Some(s) => - if (s != serverId) true + if (s == serverId) true else { - serverLog(s"serverId file contents $s does not match serverId $serverId, exiting") - interruptServer() + serverLog(s"serverId file contents $s does not match serverId $serverId") + exitServer() false } } @@ -200,7 +200,7 @@ abstract class Server[T]( // two processes and gives a spurious deadlock error while (!done && !locks.clientLock.probe()) Thread.sleep(3) - if (!idle) interruptServer() + if (!idle) exitServer() t.interrupt() // Try to give thread a moment to stop before we kill it for real diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index b67356b7664..690f256e32c 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -14,7 +14,7 @@ object ClientServerTests extends TestSuite { val ENDL = System.lineSeparator() class EchoServer(override val serverId: String, log: String => Unit, tmpDir: os.Path, locks: Locks) extends Server[Option[Int]](tmpDir, 1000, locks) with Runnable { - def interruptServer() = () + def exitServer() = log(serverId + " exiting server") def stateCache0 = None override def serverLog(s: String) = { @@ -103,6 +103,9 @@ object ClientServerTests extends TestSuite { def apply(env: Map[String, String], args: Array[String]) = runClient(tmpDir, locks)(env, args) + def logsFor(suffix: String) = { + logs.collect{case s if s.endsWith(" " + suffix) => s.dropRight(1 + suffix.length)} + } } def tests = Tests { @@ -149,8 +152,8 @@ object ClientServerTests extends TestSuite { tester.locks.processLock.probe() ) - val exitingServerLogs = tester.logs.collect{case s"$serverId exiting server" => serverId} - assert(exitingServerLogs == Seq("server-0")) + assert(tester.logsFor("Interrupting after 1000ms") == Seq("server-0")) + assert(tester.logsFor("exiting server") == Seq("server-0")) // Have a third client spawn/connect-to a new server at the same path val (serverDir3, out3, err3) = tester(Map(), Array(" World")) @@ -163,9 +166,7 @@ object ClientServerTests extends TestSuite { os.remove.all(serverDir3) Thread.sleep(500) - val missingServerIdLogs = - tester.logs.collect{case s"$serverId serverId file missing, exiting" => serverId} - assert(missingServerIdLogs == Seq("server-1")) + assert(tester.logsFor("serverId file missing") == Seq("server-0", "server-1")) } } diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index 4a1aaf75041..c97935f56f7 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -49,7 +49,7 @@ class MillServerMain( locks ) { - def interruptServer() = System.exit(Util.ExitServerCodeWhenIdle()) + def exitServer() = System.exit(Util.ExitServerCodeWhenIdle()) def stateCache0 = RunnerState.empty def main0( From e454528d6d886de3742f0042c680ecc0fe84cd57 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 21:26:16 -0700 Subject: [PATCH 22/34] refactor --- main/server/src/mill/main/server/Server.scala | 7 +-- .../mill/main/server/ClientServerTests.scala | 44 +++++++++++-------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index 971cb0d6ca3..0a91d6d18f8 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -26,7 +26,8 @@ abstract class Server[T]( locks: Locks ) { - def exitServer(): Unit + @volatile var running = true + def exitServer(): Unit = running = false var stateCache = stateCache0 def stateCache0: T @@ -40,7 +41,7 @@ abstract class Server[T]( try Server.tryLockBlock(locks.processLock) { watchServerIdFile() - while ({ + while (running && { serverLog("listening on socket") val serverSocket = bindSocket() try interruptWithTimeout(() => serverSocket.close(), () => serverSocket.accept()) match { @@ -83,7 +84,7 @@ abstract class Server[T]( os.write.over(serverDir / ServerFiles.serverId, serverId) val serverIdThread = new Thread( () => - while ( { + while (running && { Thread.sleep(100) Try(os.read(serverDir / ServerFiles.serverId)).toOption match { case None => diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 690f256e32c..6e32f9910fa 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -14,7 +14,10 @@ object ClientServerTests extends TestSuite { val ENDL = System.lineSeparator() class EchoServer(override val serverId: String, log: String => Unit, tmpDir: os.Path, locks: Locks) extends Server[Option[Int]](tmpDir, 1000, locks) with Runnable { - def exitServer() = log(serverId + " exiting server") + override def exitServer() = { + log(serverId + " exiting server") + super.exitServer() + } def stateCache0 = None override def serverLog(s: String) = { @@ -97,7 +100,7 @@ object ClientServerTests extends TestSuite { env.asJava ) - (serverDir, new String(out.toByteArray), new String(err.toByteArray)) + ClientResult(serverDir, new String(out.toByteArray), new String(err.toByteArray)) } } @@ -108,6 +111,8 @@ object ClientServerTests extends TestSuite { } } + case class ClientResult(serverDir: os.Path, out: String, err: String) + def tests = Tests { "hello" - { val tester = new Tester @@ -121,11 +126,11 @@ object ClientServerTests extends TestSuite { tester.locks.processLock.probe() ) - val (_, out1, err1) = tester(Map(), Array("world")) + val res1 = tester(Map(), Array("world")) assert( - out1 == s"helloworld${ENDL}", - err1 == s"HELLOworld${ENDL}" + res1.out == s"helloworld${ENDL}", + res1.err == s"HELLOworld${ENDL}" ) // Give a bit of time for the server to release the lock and @@ -137,11 +142,11 @@ object ClientServerTests extends TestSuite { ) // A seecond client in sequence connect to the same server - val (_, out2, err2) = tester(Map(), Array(" WORLD")) + val res2 = tester(Map(), Array(" WORLD")) assert( - out2 == s"hello WORLD${ENDL}", - err2 == s"HELLO WORLD${ENDL}" + res2.out == s"hello WORLD${ENDL}", + res2.err == s"HELLO WORLD${ENDL}" ) if (!Util.isWindows) { @@ -156,17 +161,18 @@ object ClientServerTests extends TestSuite { assert(tester.logsFor("exiting server") == Seq("server-0")) // Have a third client spawn/connect-to a new server at the same path - val (serverDir3, out3, err3) = tester(Map(), Array(" World")) + val res3 = tester(Map(), Array(" World")) assert( - out3 == s"hello World${ENDL}", - err3 == s"HELLO World${ENDL}" + res3.out == s"hello World${ENDL}", + res3.err == s"HELLO World${ENDL}" ) // Make sure if we delete the out dir, the server notices and exits - os.remove.all(serverDir3) + os.remove.all(res3.serverDir) Thread.sleep(500) - assert(tester.logsFor("serverId file missing") == Seq("server-0", "server-1")) + assert(tester.logsFor("serverId file missing") == Seq("server-1")) + assert(tester.logsFor("exiting server") == Seq("server-0", "server-1")) } } @@ -193,12 +199,12 @@ object ClientServerTests extends TestSuite { "c" -> c1000 ) - val (_, out1, err1) = tester(env, Array()) + val res1 = tester(env, Array()) val expected = s"a=$a1000${ENDL}b=$b1000${ENDL}c=$c1000${ENDL}" assert( - out1 == expected, - err1 == "" + res1.out == expected, + res1.err == "" ) assert( @@ -229,13 +235,13 @@ object ClientServerTests extends TestSuite { ) val pathEnvVar = path.mkString(":") - val (_, out2, err2) = tester(Map("PATH" -> pathEnvVar), Array()) + val res2 = tester(Map("PATH" -> pathEnvVar), Array()) val expected2 = s"PATH=$pathEnvVar${ENDL}" assert( - out2 == expected2, - err2 == "" + res2.out == expected2, + res2.err == "" ) } } From 5969043746b2efca61c7729de5461098b8587bcf Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 21:52:34 -0700 Subject: [PATCH 23/34] add more logging --- main/server/src/mill/main/server/Server.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index 0a91d6d18f8..80bb195bc31 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -165,6 +165,8 @@ abstract class Server[T]( } val args = Util.parseArgs(argStream) val env = Util.parseMap(argStream) + serverLog("args " + upickle.default.write(args)) + serverLog("env " + upickle.default.write(env.asScala)) val userSpecifiedProperties = Util.parseMap(argStream) argStream.close() @@ -185,10 +187,8 @@ abstract class Server[T]( ) stateCache = newStateCache - os.write.over( - serverDir / ServerFiles.exitCode, - (if (result) 0 else 1).toString.getBytes() - ) + serverLog("exitCode " + ServerFiles.exitCode) + os.write.over(serverDir / ServerFiles.exitCode, if (result) "0" else "1") } finally { done = true idle = true From 59d962561ed4d85f4893bf09762d5b6d941f0044 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 22:18:53 -0700 Subject: [PATCH 24/34] . --- .../src/mill/main/client/ServerLauncher.java | 6 ++-- .../mill/main/server/ClientServerTests.scala | 32 ++++++++----------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/main/client/src/mill/main/client/ServerLauncher.java b/main/client/src/mill/main/client/ServerLauncher.java index e8a6bbd038c..6ff1fa5cb66 100644 --- a/main/client/src/mill/main/client/ServerLauncher.java +++ b/main/client/src/mill/main/client/ServerLauncher.java @@ -59,8 +59,8 @@ public static int runMain(String[] args, BiConsumer initServer) while (serverIndex < serverProcessesLimit) { // Try each possible server process (-1 to -5) serverIndex++; final String serverDir = out + "/" + millWorker + versionAndJvmHomeEncoding + "-" + serverIndex; - java.io.File lockBaseFile = new java.io.File(serverDir); - lockBaseFile.mkdirs(); + java.io.File serverDirFile = new java.io.File(serverDir); + serverDirFile.mkdirs(); int lockAttempts = 0; while (lockAttempts < maxLockAttempts) { // Try to lock a particular server @@ -78,7 +78,7 @@ public static int runMain(String[] args, BiConsumer initServer) ); } } catch (Exception e) { - for (File file : lockBaseFile.listFiles()) file.delete(); + for (File file : serverDirFile.listFiles()) file.delete(); } finally { lockAttempts++; } diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 6e32f9910fa..3285715158b 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -9,11 +9,15 @@ import scala.jdk.CollectionConverters._ import utest._ +/** + * Exercises the client-server logic in memory, using in-memory locks + * and in-memory clients and servers + */ object ClientServerTests extends TestSuite { val ENDL = System.lineSeparator() - class EchoServer(override val serverId: String, log: String => Unit, tmpDir: os.Path, locks: Locks) - extends Server[Option[Int]](tmpDir, 1000, locks) with Runnable { + class EchoServer(override val serverId: String, log: String => Unit, serverDir: os.Path, locks: Locks) + extends Server[Option[Int]](serverDir, 1000, locks) with Runnable { override def exitServer() = { log(serverId + " exiting server") super.exitServer() @@ -60,22 +64,14 @@ object ClientServerTests extends TestSuite { val terminatedServers = collection.mutable.Set.empty[String] val dest = os.pwd / "out" os.makeDir.all(dest) - val tmpDir = os.temp.dir(dest) + val serverDir = os.temp.dir(dest) val locks = Locks.memory() - - def initStreams() = { - val in = new ByteArrayInputStream(s"hello${ENDL}".getBytes()) - val out = new ByteArrayOutputStream() - val err = new ByteArrayOutputStream() - (in, out, err) - } - - def spawnEchoServer(tmpDir: os.Path, locks: Locks): Unit = { + def spawnEchoServer(serverDir: os.Path, locks: Locks): Unit = { val serverId = "server-" + nextServerId nextServerId += 1 - new Thread(new EchoServer(serverId, log, tmpDir, locks)).start() + new Thread(new EchoServer(serverId, log, serverDir, locks)).start() } def log(s: String) = { logs.append(s) @@ -84,10 +80,10 @@ object ClientServerTests extends TestSuite { val logs = collection.mutable.Buffer.empty[String] - def runClient(serverDir: os.Path, - locks: Locks - )(env: Map[String, String], args: Array[String]) = { - val (in, out, err) = initStreams() + def runClient(env: Map[String, String], args: Array[String]) = { + val in = new ByteArrayInputStream(s"hello${ENDL}".getBytes()) + val out = new ByteArrayOutputStream() + val err = new ByteArrayOutputStream() Server.lockBlock(locks.clientLock) { mill.main.client.ServerLauncher.run( serverDir.toString, @@ -104,7 +100,7 @@ object ClientServerTests extends TestSuite { } } - def apply(env: Map[String, String], args: Array[String]) = runClient(tmpDir, locks)(env, args) + def apply(env: Map[String, String], args: Array[String]) = runClient(env, args) def logsFor(suffix: String) = { logs.collect{case s if s.endsWith(" " + suffix) => s.dropRight(1 + suffix.length)} From 4985f04a3492db9c2ae021cb3906818732c210ae Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Aug 2024 23:54:36 -0700 Subject: [PATCH 25/34] . --- .../mill/main/client/FileToStreamTailer.java | 1 + .../src/mill/main/client/ServerFiles.java | 2 +- .../src/mill/main/client/ServerLauncher.java | 189 +++++++++--------- .../src/mill/main/client/lock/FileLock.java | 2 + .../src/mill/main/client/lock/Lock.java | 1 + .../src/mill/main/client/lock/Locks.java | 4 +- main/server/src/mill/main/server/Server.scala | 5 +- .../mill/main/server/ClientServerTests.scala | 118 +++++------ .../mill/runner/client/MillClientMain.java | 18 +- runner/src/mill/runner/MillServerMain.scala | 2 +- 10 files changed, 161 insertions(+), 181 deletions(-) diff --git a/main/client/src/mill/main/client/FileToStreamTailer.java b/main/client/src/mill/main/client/FileToStreamTailer.java index f401f062ecb..9847f5db6d0 100644 --- a/main/client/src/mill/main/client/FileToStreamTailer.java +++ b/main/client/src/mill/main/client/FileToStreamTailer.java @@ -99,6 +99,7 @@ public void flush() { @Override public void close() throws Exception { + flush(); interrupt(); } } diff --git a/main/client/src/mill/main/client/ServerFiles.java b/main/client/src/mill/main/client/ServerFiles.java index 20e280a857f..208237ae6e6 100644 --- a/main/client/src/mill/main/client/ServerFiles.java +++ b/main/client/src/mill/main/client/ServerFiles.java @@ -31,7 +31,7 @@ public class ServerFiles { */ public static String pipe(String base) { try { - return base + "/mill-" + Util.md5hex(new java.io.File(base).getCanonicalPath()) + "-io"; + return base + "/mill-" + Util.md5hex(new java.io.File(base).getCanonicalPath()).substring(0, 8) + "-io"; }catch (Exception e){ throw new RuntimeException(e); } diff --git a/main/client/src/mill/main/client/ServerLauncher.java b/main/client/src/mill/main/client/ServerLauncher.java index 6ff1fa5cb66..507ac6c69ad 100644 --- a/main/client/src/mill/main/client/ServerLauncher.java +++ b/main/client/src/mill/main/client/ServerLauncher.java @@ -11,6 +11,7 @@ import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; +import java.io.PrintStream; import java.net.Socket; import java.nio.file.Files; import java.nio.file.Paths; @@ -42,10 +43,34 @@ * - Wait for `ProxyStream.END` packet or `clientSocket.close()`, * indicating server has finished execution and all data has been received */ -public class ServerLauncher { +public abstract class ServerLauncher { + public static class Result{ + public int exitCode; + public String serverDir; + } final static int tailerRefreshIntervalMillis = 2; final static int maxLockAttempts = 3; - public static int runMain(String[] args, BiConsumer initServer) throws Exception { + public abstract void initServer(String serverDir, boolean b, Locks locks) throws Exception; + InputStream stdin; + PrintStream stdout; + PrintStream stderr; + Map env; + String[] args; + Locks[] memoryLocks; + public ServerLauncher(InputStream stdin, + PrintStream stdout, + PrintStream stderr, + Map env, + String[] args, + Locks[] memoryLocks){ + this.stdin = stdin; + this.stdout = stdout; + this.stderr = stderr; + this.env = env; + this.args = args; + this.memoryLocks = memoryLocks; + } + public Result acquireLocksAndRun(String outDir) throws Exception { final boolean setJnaNoSys = System.getProperty("jna.nosys") == null; if (setJnaNoSys) { @@ -58,24 +83,21 @@ public static int runMain(String[] args, BiConsumer initServer) int serverIndex = 0; while (serverIndex < serverProcessesLimit) { // Try each possible server process (-1 to -5) serverIndex++; - final String serverDir = out + "/" + millWorker + versionAndJvmHomeEncoding + "-" + serverIndex; + final String serverDir = outDir + "/" + millWorker + versionAndJvmHomeEncoding + "-" + serverIndex; java.io.File serverDirFile = new java.io.File(serverDir); serverDirFile.mkdirs(); int lockAttempts = 0; while (lockAttempts < maxLockAttempts) { // Try to lock a particular server try ( - Locks locks = Locks.files(serverDir); + Locks locks = memoryLocks != null ? memoryLocks[serverIndex-1] : Locks.files(serverDir); TryLocked clientLock = locks.clientLock.tryLock() ) { if (clientLock != null) { - return runMillServer( - args, - serverDir, - setJnaNoSys, - locks, - () -> initServer.accept(serverDir, setJnaNoSys) - ); + Result result = new Result(); + result.exitCode = run(serverDir, setJnaNoSys, locks); + result.serverDir = serverDir; + return result; } } catch (Exception e) { for (File file : serverDirFile.listFiles()) file.delete(); @@ -87,39 +109,6 @@ public static int runMain(String[] args, BiConsumer initServer) throw new ServerCouldNotBeStarted("Reached max server processes limit: " + serverProcessesLimit); } - static int runMillServer(String[] args, - String serverDir, - boolean setJnaNoSys, - Locks locks, - Runnable initServer) throws Exception { - final File stdout = new java.io.File(serverDir + "/" + ServerFiles.stdout); - final File stderr = new java.io.File(serverDir + "/" + ServerFiles.stderr); - - try( - final FileToStreamTailer stdoutTailer = new FileToStreamTailer(stdout, System.out, tailerRefreshIntervalMillis); - final FileToStreamTailer stderrTailer = new FileToStreamTailer(stderr, System.err, tailerRefreshIntervalMillis); - ) { - stdoutTailer.start(); - stderrTailer.start(); - final int exitCode = run( - serverDir, - initServer, - locks, - System.in, - System.out, - System.err, - args, - System.getenv()); - - // Here, we ensure we process the tails of the output files before interrupting - // the threads - stdoutTailer.flush(); - stderrTailer.flush(); - - return exitCode; - } - } - // 5 processes max private static int getServerProcessesLimit(String jvmHomeEncoding) { File outFolder = new File(out); @@ -136,66 +125,76 @@ private static int getServerProcessesLimit(String jvmHomeEncoding) { return processLimit; } - public static int run( - String serverDir, - Runnable initServer, - Locks locks, - InputStream stdin, - OutputStream stdout, - OutputStream stderr, - String[] args, - Map env) throws Exception { - - try (FileOutputStream f = new FileOutputStream(serverDir + "/" + ServerFiles.runArgs)) { - f.write(System.console() != null ? 1 : 0); - Util.writeString(f, BuildInfo.millVersion); - Util.writeArgs(args, f); - Util.writeMap(env, f); - } - if (locks.processLock.probe()) initServer.run(); + int run(String serverDir, boolean setJnaNoSys, Locks locks) throws Exception { + try( + final FileToStreamTailer stdoutTailer = new FileToStreamTailer( + new java.io.File(serverDir + "/" + ServerFiles.stdout), + stdout, + tailerRefreshIntervalMillis + ); + final FileToStreamTailer stderrTailer = new FileToStreamTailer( + new java.io.File(serverDir + "/" + ServerFiles.stderr), + stderr, + tailerRefreshIntervalMillis + ); + ) { + stdoutTailer.start(); + stderrTailer.start(); + try (FileOutputStream f = new FileOutputStream(serverDir + "/" + ServerFiles.runArgs)) { + f.write(System.console() != null ? 1 : 0); + Util.writeString(f, BuildInfo.millVersion); + Util.writeArgs(args, f); + Util.writeMap(env, f); + } + + if (locks.processLock.probe()) { + initServer(serverDir, setJnaNoSys, locks); + } + + while (locks.processLock.probe()) Thread.sleep(3); + + String socketName = ServerFiles.pipe(serverDir); + AFUNIXSocketAddress addr = AFUNIXSocketAddress.of(new File(socketName)); + + long retryStart = System.currentTimeMillis(); + Socket ioSocket = null; + Throwable socketThrowable = null; + while (ioSocket == null && System.currentTimeMillis() - retryStart < 1000) { + try { + ioSocket = AFUNIXSocket.connectTo(addr); + } catch (Throwable e) { + socketThrowable = e; + Thread.sleep(10); + } + } + + if (ioSocket == null) { + throw new Exception("Failed to connect to server", socketThrowable); + } - while (locks.processLock.probe()) Thread.sleep(3); + InputStream outErr = ioSocket.getInputStream(); + OutputStream in = ioSocket.getOutputStream(); + ProxyStream.Pumper outPumper = new ProxyStream.Pumper(outErr, stdout, stderr); + InputPumper inPump = new InputPumper(() -> stdin, () -> in, true); + Thread outPumperThread = new Thread(outPumper, "outPump"); + outPumperThread.setDaemon(true); + Thread inThread = new Thread(inPump, "inPump"); + inThread.setDaemon(true); + outPumperThread.start(); + inThread.start(); - String socketName = ServerFiles.pipe(serverDir); - AFUNIXSocketAddress addr = AFUNIXSocketAddress.of(new File(socketName)); + outPumperThread.join(); - long retryStart = System.currentTimeMillis(); - Socket ioSocket = null; - Throwable socketThrowable = null; - while (ioSocket == null && System.currentTimeMillis() - retryStart < 1000) { try { - ioSocket = AFUNIXSocket.connectTo(addr); + return Integer.parseInt(Files.readAllLines(Paths.get(serverDir + "/" + ServerFiles.exitCode)).get(0)); } catch (Throwable e) { - socketThrowable = e; - Thread.sleep(1); + return Util.ExitClientCodeCannotReadFromExitCodeFile(); + } finally { + ioSocket.close(); } } - if (ioSocket == null) { - throw new Exception("Failed to connect to server", socketThrowable); - } - - InputStream outErr = ioSocket.getInputStream(); - OutputStream in = ioSocket.getOutputStream(); - ProxyStream.Pumper outPumper = new ProxyStream.Pumper(outErr, stdout, stderr); - InputPumper inPump = new InputPumper(() -> stdin, () -> in, true); - Thread outPumperThread = new Thread(outPumper, "outPump"); - outPumperThread.setDaemon(true); - Thread inThread = new Thread(inPump, "inPump"); - inThread.setDaemon(true); - outPumperThread.start(); - inThread.start(); - - outPumperThread.join(); - - try { - return Integer.parseInt(Files.readAllLines(Paths.get(serverDir + "/" + ServerFiles.exitCode)).get(0)); - } catch (Throwable e) { - return Util.ExitClientCodeCannotReadFromExitCodeFile(); - } finally { - ioSocket.close(); - } } } diff --git a/main/client/src/mill/main/client/lock/FileLock.java b/main/client/src/mill/main/client/lock/FileLock.java index a405bc03c13..454b8794bd7 100644 --- a/main/client/src/mill/main/client/lock/FileLock.java +++ b/main/client/src/mill/main/client/lock/FileLock.java @@ -35,4 +35,6 @@ public void close() throws Exception { chan.close(); raf.close(); } + + public void delete() throws Exception { close(); } } diff --git a/main/client/src/mill/main/client/lock/Lock.java b/main/client/src/mill/main/client/lock/Lock.java index 72674cb42c8..6d729c0ebd6 100644 --- a/main/client/src/mill/main/client/lock/Lock.java +++ b/main/client/src/mill/main/client/lock/Lock.java @@ -14,4 +14,5 @@ public void await() throws Exception { * Returns `true` if the lock is *available for taking* */ public abstract boolean probe() throws Exception; + public void delete() throws Exception {} } diff --git a/main/client/src/mill/main/client/lock/Locks.java b/main/client/src/mill/main/client/lock/Locks.java index 6de46cb0651..b026fc17393 100644 --- a/main/client/src/mill/main/client/lock/Locks.java +++ b/main/client/src/mill/main/client/lock/Locks.java @@ -29,7 +29,7 @@ public static Locks memory() { @Override public void close() throws Exception { - clientLock.close(); - processLock.close(); + clientLock.delete(); + processLock.delete(); } } diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index 80bb195bc31..2563515e0fd 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -32,7 +32,10 @@ abstract class Server[T]( def stateCache0: T val serverId = java.lang.Long.toHexString(scala.util.Random.nextLong()) - def serverLog(s: String) = println(serverDir / ServerFiles.serverLog, s + "\n") + def serverLog0(s: String): Unit = + os.write.append(serverDir / ServerFiles.serverLog, s"$s\n", createFolders = true) + + def serverLog(s: String): Unit = serverLog0(s"$serverId $s") def run(): Unit = { serverLog("running server") diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 3285715158b..f60363e140c 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -2,6 +2,7 @@ package mill.main.server import java.io._ import mill.main.client.Util +import mill.main.client.ServerFiles import mill.main.client.lock.Locks import mill.api.SystemStreams @@ -16,17 +17,19 @@ import utest._ object ClientServerTests extends TestSuite { val ENDL = System.lineSeparator() - class EchoServer(override val serverId: String, log: String => Unit, serverDir: os.Path, locks: Locks) + class EchoServer(override val serverId: String, serverDir: os.Path, locks: Locks) extends Server[Option[Int]](serverDir, 1000, locks) with Runnable { override def exitServer() = { - log(serverId + " exiting server") + serverLog("exiting server") super.exitServer() } def stateCache0 = None - override def serverLog(s: String) = { - log(serverId + " " + s) + override def serverLog0(s: String) = { + println(s) + super.serverLog0(s) } + def main0( args: Array[String], stateCache: Option[Int], @@ -64,64 +67,57 @@ object ClientServerTests extends TestSuite { val terminatedServers = collection.mutable.Set.empty[String] val dest = os.pwd / "out" os.makeDir.all(dest) - val serverDir = os.temp.dir(dest) - - val locks = Locks.memory() - - def spawnEchoServer(serverDir: os.Path, locks: Locks): Unit = { - val serverId = "server-" + nextServerId - nextServerId += 1 - new Thread(new EchoServer(serverId, log, serverDir, locks)).start() - } - def log(s: String) = { - logs.append(s) - println(s) - } - val logs = collection.mutable.Buffer.empty[String] + val outDir = os.temp.dir(dest, deleteOnExit = false) + val memoryLocks = Array.fill(10)(Locks.memory()); def runClient(env: Map[String, String], args: Array[String]) = { val in = new ByteArrayInputStream(s"hello${ENDL}".getBytes()) val out = new ByteArrayOutputStream() val err = new ByteArrayOutputStream() - Server.lockBlock(locks.clientLock) { - mill.main.client.ServerLauncher.run( - serverDir.toString, - () => spawnEchoServer(serverDir, locks), - locks, - in, - out, - err, - args, - env.asJava - ) - - ClientResult(serverDir, new String(out.toByteArray), new String(err.toByteArray)) - } + val result = new mill.main.client.ServerLauncher( + in, + new PrintStream(out), + new PrintStream(err), + env.asJava, + args, + memoryLocks + ){ + def initServer(serverDir: String, b: Boolean, locks: Locks) = { + val serverId = "server-" + nextServerId + nextServerId += 1 + new Thread(new EchoServer(serverId, os.Path(serverDir, os.pwd), locks)).start() + } + }.acquireLocksAndRun(outDir.relativeTo(os.pwd).toString) + + ClientResult( + result.exitCode, + os.Path(result.serverDir, os.pwd), + outDir, + out.toString, + err.toString + ) } def apply(env: Map[String, String], args: Array[String]) = runClient(env, args) + } + case class ClientResult(exitCode: Int, + serverDir: os.Path, + outDir: os.Path, + out: String, + err: String){ def logsFor(suffix: String) = { - logs.collect{case s if s.endsWith(" " + suffix) => s.dropRight(1 + suffix.length)} + os.read + .lines(serverDir / ServerFiles.serverLog) + .collect{case s if s.endsWith(" " + suffix) => s.dropRight(1 + suffix.length)} } } - case class ClientResult(serverDir: os.Path, out: String, err: String) - def tests = Tests { "hello" - { val tester = new Tester - - // Make sure the simple "have the client start a server and - // exchange one message" workflow works from end to end. - - assert( - tester.locks.clientLock.probe(), - tester.locks.processLock.probe() - ) - val res1 = tester(Map(), Array("world")) assert( @@ -129,15 +125,7 @@ object ClientServerTests extends TestSuite { res1.err == s"HELLOworld${ENDL}" ) - // Give a bit of time for the server to release the lock and - // re-acquire it to signal to the client that it"s" done - - assert( - tester.locks.clientLock.probe(), - !tester.locks.processLock.probe() - ) - - // A seecond client in sequence connect to the same server + // A second client in sequence connect to the same server val res2 = tester(Map(), Array(" WORLD")) assert( @@ -148,13 +136,9 @@ object ClientServerTests extends TestSuite { if (!Util.isWindows) { // Make sure the server times out of not used for a while Thread.sleep(2000) - assert( - tester.locks.clientLock.probe(), - tester.locks.processLock.probe() - ) - assert(tester.logsFor("Interrupting after 1000ms") == Seq("server-0")) - assert(tester.logsFor("exiting server") == Seq("server-0")) + assert(res2.logsFor("Interrupting after 1000ms") == Seq("server-0")) + assert(res2.logsFor("exiting server") == Seq("server-0")) // Have a third client spawn/connect-to a new server at the same path val res3 = tester(Map(), Array(" World")) @@ -164,11 +148,11 @@ object ClientServerTests extends TestSuite { ) // Make sure if we delete the out dir, the server notices and exits - os.remove.all(res3.serverDir) + os.remove.all(res3.outDir) Thread.sleep(500) - assert(tester.logsFor("serverId file missing") == Seq("server-1")) - assert(tester.logsFor("exiting server") == Seq("server-0", "server-1")) + assert(res3.logsFor("serverId file missing") == Seq("server-1")) + assert(res3.logsFor("exiting server") == Seq("server-1")) } } @@ -179,11 +163,6 @@ object ClientServerTests extends TestSuite { // Make sure the simple "have the client start a server and // exchange one message" workflow works from end to end. - assert( - tester.locks.clientLock.probe(), - tester.locks.processLock.probe() - ) - def longString(s: String) = Array.fill(1000)(s).mkString val b1000 = longString("b") val c1000 = longString("c") @@ -203,11 +182,6 @@ object ClientServerTests extends TestSuite { res1.err == "" ) - assert( - tester.locks.clientLock.probe(), - !tester.locks.processLock.probe() - ) - val path = List( "/Users/foo/Library/Haskell/bin", "/usr/local/git/bin", diff --git a/runner/client/src/mill/runner/client/MillClientMain.java b/runner/client/src/mill/runner/client/MillClientMain.java index f9dcb9bc394..2d48cc4b507 100644 --- a/runner/client/src/mill/runner/client/MillClientMain.java +++ b/runner/client/src/mill/runner/client/MillClientMain.java @@ -5,6 +5,8 @@ import mill.main.client.ServerLauncher; import mill.main.client.ServerFiles; import mill.main.client.Util; +import mill.main.client.lock.Locks; +import mill.main.client.OutFiles; import mill.main.client.ServerCouldNotBeStarted; /** @@ -34,9 +36,14 @@ public static void main(String[] args) throws Exception { MillNoServerLauncher.runMain(args); } else try { // start in client-server mode - int exitCode = ServerLauncher.runMain(args, initServer); + ServerLauncher launcher = new ServerLauncher(System.in, System.out, System.err, System.getenv(), args, null){ + public void initServer(String serverDir, boolean setJnaNoSys, Locks locks) throws Exception{ + MillProcessLauncher.launchMillServer(serverDir, setJnaNoSys); + } + }; + int exitCode = launcher.acquireLocksAndRun(OutFiles.out); if (exitCode == Util.ExitServerCodeWhenVersionMismatch()) { - exitCode = ServerLauncher.runMain(args, initServer); + exitCode = launcher.acquireLocksAndRun(OutFiles.out); } System.exit(exitCode); } catch (ServerCouldNotBeStarted e) { @@ -55,11 +62,4 @@ public static void main(String[] args) throws Exception { } } - private static BiConsumer initServer = (serverDir, setJnaNoSys) -> { - try { - MillProcessLauncher.launchMillServer(serverDir, setJnaNoSys); - } catch (Exception e) { - throw new RuntimeException(e); - } - }; } diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index c97935f56f7..0779b2b222c 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -49,7 +49,7 @@ class MillServerMain( locks ) { - def exitServer() = System.exit(Util.ExitServerCodeWhenIdle()) + override def exitServer() = {super.exitServer(); System.exit(Util.ExitServerCodeWhenIdle())} def stateCache0 = RunnerState.empty def main0( From e5176c71c2b70eaf7419e1479aa9d93db0d2691a Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 15 Aug 2024 00:17:30 -0700 Subject: [PATCH 26/34] concurrency test --- .../src/mill/main/client/ServerLauncher.java | 4 +- main/server/src/mill/main/server/Server.scala | 7 +-- .../mill/main/server/ClientServerTests.scala | 49 ++++++++++++++----- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/main/client/src/mill/main/client/ServerLauncher.java b/main/client/src/mill/main/client/ServerLauncher.java index 507ac6c69ad..77487b4db65 100644 --- a/main/client/src/mill/main/client/ServerLauncher.java +++ b/main/client/src/mill/main/client/ServerLauncher.java @@ -91,9 +91,9 @@ public Result acquireLocksAndRun(String outDir) throws Exception { while (lockAttempts < maxLockAttempts) { // Try to lock a particular server try ( Locks locks = memoryLocks != null ? memoryLocks[serverIndex-1] : Locks.files(serverDir); - TryLocked clientLock = locks.clientLock.tryLock() + TryLocked clientLocked = locks.clientLock.tryLock() ) { - if (clientLock != null) { + if (clientLocked.isLocked()) { Result result = new Result(); result.exitCode = run(serverDir, setJnaNoSys, locks); result.serverDir = serverDir; diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index 2563515e0fd..0b5e68f409a 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -38,14 +38,13 @@ abstract class Server[T]( def serverLog(s: String): Unit = serverLog0(s"$serverId $s") def run(): Unit = { - serverLog("running server") + serverLog("running server in " + serverDir) val initialSystemProperties = sys.props.toMap try Server.tryLockBlock(locks.processLock) { watchServerIdFile() while (running && { - serverLog("listening on socket") val serverSocket = bindSocket() try interruptWithTimeout(() => serverSocket.close(), () => serverSocket.accept()) match { case None => false @@ -67,8 +66,10 @@ abstract class Server[T]( val socketPath = os.Path(ServerFiles.pipe(serverDir.toString())) os.remove.all(socketPath) + val relFile = socketPath.relativeTo(os.pwd).toNIO.toFile + serverLog("listening on socket " + relFile) // Use relative path because otherwise the full path might be too long for the socket API - val addr = AFUNIXSocketAddress.of(socketPath.relativeTo(os.pwd).toNIO.toFile) + val addr = AFUNIXSocketAddress.of(relFile) AFUNIXServerSocket.bindOn(addr) } diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index f60363e140c..41b0ceb5fd2 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -43,6 +43,7 @@ object ClientServerTests extends TestSuite { val reader = new BufferedReader(new InputStreamReader(streams.in)) val str = reader.readLine() + Thread.sleep(200) if (args.nonEmpty) { streams.out.println(str + args(0)) } @@ -72,7 +73,7 @@ object ClientServerTests extends TestSuite { val memoryLocks = Array.fill(10)(Locks.memory()); def runClient(env: Map[String, String], args: Array[String]) = { - val in = new ByteArrayInputStream(s"hello${ENDL}".getBytes()) + val in = new ByteArrayInputStream(s"hello$ENDL".getBytes()) val out = new ByteArrayOutputStream() val err = new ByteArrayOutputStream() val result = new mill.main.client.ServerLauncher( @@ -115,22 +116,22 @@ object ClientServerTests extends TestSuite { } def tests = Tests { + val tester = new Tester "hello" - { - val tester = new Tester val res1 = tester(Map(), Array("world")) assert( - res1.out == s"helloworld${ENDL}", - res1.err == s"HELLOworld${ENDL}" + res1.out == s"helloworld$ENDL", + res1.err == s"HELLOworld$ENDL" ) // A second client in sequence connect to the same server val res2 = tester(Map(), Array(" WORLD")) assert( - res2.out == s"hello WORLD${ENDL}", - res2.err == s"HELLO WORLD${ENDL}" + res2.out == s"hello WORLD$ENDL", + res2.err == s"HELLO WORLD$ENDL" ) if (!Util.isWindows) { @@ -143,8 +144,8 @@ object ClientServerTests extends TestSuite { // Have a third client spawn/connect-to a new server at the same path val res3 = tester(Map(), Array(" World")) assert( - res3.out == s"hello World${ENDL}", - res3.err == s"HELLO World${ENDL}" + res3.out == s"hello World$ENDL", + res3.err == s"HELLO World$ENDL" ) // Make sure if we delete the out dir, the server notices and exits @@ -153,13 +154,35 @@ object ClientServerTests extends TestSuite { assert(res3.logsFor("serverId file missing") == Seq("server-1")) assert(res3.logsFor("exiting server") == Seq("server-1")) - } } - "envVars" - retry(3) { - val tester = new Tester + "concurrency" - { + // Make sure concurrently running client commands results in multiple processes + // being spawned, running in different folders + println("="*80) + import concurrent._ + import concurrent.ExecutionContext.Implicits.global + val f1 = Future(tester(Map(), Array(" World"))) + val f2 = Future(tester(Map(), Array(" WORLD"))) + val f3 = Future(tester(Map(), Array(" wOrLd"))) + val resF1 = Await.result(f1, duration.Duration.Inf) + val resF2 = Await.result(f2, duration.Duration.Inf) + val resF3 = Await.result(f3, duration.Duration.Inf) + + // Mutiple server processes live in same out folder + assert(resF1.outDir == resF2.outDir) + assert(resF2.outDir == resF3.outDir) + // but the serverDir is placed in different subfolders + assert(resF1.serverDir != resF2.serverDir) + assert(resF2.serverDir != resF3.serverDir) + + assert(resF1.out == s"hello World$ENDL") + assert(resF2.out == s"hello WORLD$ENDL") + assert(resF3.out == s"hello wOrLd$ENDL") + } + "envVars" - retry(3) { // Make sure the simple "have the client start a server and // exchange one message" workflow works from end to end. @@ -175,7 +198,7 @@ object ClientServerTests extends TestSuite { ) val res1 = tester(env, Array()) - val expected = s"a=$a1000${ENDL}b=$b1000${ENDL}c=$c1000${ENDL}" + val expected = s"a=$a1000${ENDL}b=$b1000${ENDL}c=$c1000$ENDL" assert( res1.out == expected, @@ -207,7 +230,7 @@ object ClientServerTests extends TestSuite { val pathEnvVar = path.mkString(":") val res2 = tester(Map("PATH" -> pathEnvVar), Array()) - val expected2 = s"PATH=$pathEnvVar${ENDL}" + val expected2 = s"PATH=$pathEnvVar$ENDL" assert( res2.out == expected2, From 0787023c71c2d16069d5cf36cdca2b19e73e5661 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 15 Aug 2024 00:25:39 -0700 Subject: [PATCH 27/34] . --- .../src/mill/main/client/ServerLauncher.java | 52 ++++++------------- .../mill/runner/client/MillClientMain.java | 4 +- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/main/client/src/mill/main/client/ServerLauncher.java b/main/client/src/mill/main/client/ServerLauncher.java index 77487b4db65..8e47a367ef2 100644 --- a/main/client/src/mill/main/client/ServerLauncher.java +++ b/main/client/src/mill/main/client/ServerLauncher.java @@ -49,7 +49,8 @@ public static class Result{ public String serverDir; } final static int tailerRefreshIntervalMillis = 2; - final static int maxLockAttempts = 3; + final int serverProcessesLimit = 5; + final int serverInitWaitMillis = 10000; public abstract void initServer(String serverDir, boolean b, Locks locks) throws Exception; InputStream stdin; PrintStream stdout; @@ -68,6 +69,9 @@ public ServerLauncher(InputStream stdin, this.stderr = stderr; this.env = env; this.args = args; + // For testing in memory, we need to pass in the locks separately, so that the + // locks can be shared between the different instances of `ServerLauncher` the + // same way file locks are shared between different Mill client/secrer processes this.memoryLocks = memoryLocks; } public Result acquireLocksAndRun(String outDir) throws Exception { @@ -78,7 +82,7 @@ public Result acquireLocksAndRun(String outDir) throws Exception { } final String versionAndJvmHomeEncoding = Util.sha1Hash(BuildInfo.millVersion + System.getProperty("java.home")); - final int serverProcessesLimit = getServerProcessesLimit(versionAndJvmHomeEncoding); + int serverIndex = 0; while (serverIndex < serverProcessesLimit) { // Try each possible server process (-1 to -5) @@ -87,45 +91,21 @@ public Result acquireLocksAndRun(String outDir) throws Exception { java.io.File serverDirFile = new java.io.File(serverDir); serverDirFile.mkdirs(); - int lockAttempts = 0; - while (lockAttempts < maxLockAttempts) { // Try to lock a particular server - try ( - Locks locks = memoryLocks != null ? memoryLocks[serverIndex-1] : Locks.files(serverDir); - TryLocked clientLocked = locks.clientLock.tryLock() - ) { - if (clientLocked.isLocked()) { - Result result = new Result(); - result.exitCode = run(serverDir, setJnaNoSys, locks); - result.serverDir = serverDir; - return result; - } - } catch (Exception e) { - for (File file : serverDirFile.listFiles()) file.delete(); - } finally { - lockAttempts++; + try ( + Locks locks = memoryLocks != null ? memoryLocks[serverIndex-1] : Locks.files(serverDir); + TryLocked clientLocked = locks.clientLock.tryLock() + ) { + if (clientLocked.isLocked()) { + Result result = new Result(); + result.exitCode = run(serverDir, setJnaNoSys, locks); + result.serverDir = serverDir; + return result; } } } throw new ServerCouldNotBeStarted("Reached max server processes limit: " + serverProcessesLimit); } - // 5 processes max - private static int getServerProcessesLimit(String jvmHomeEncoding) { - File outFolder = new File(out); - String[] totalProcesses = outFolder.list((dir, name) -> name.startsWith(millWorker)); - String[] thisJdkProcesses = outFolder.list((dir, name) -> name.startsWith(millWorker + jvmHomeEncoding)); - - int processLimit = 5; - - if (thisJdkProcesses != null) { - processLimit -= Math.min(totalProcesses.length - thisJdkProcesses.length, 5); - } else if (totalProcesses != null) { - processLimit -= Math.min(totalProcesses.length, 5); - } - - return processLimit; - } - int run(String serverDir, boolean setJnaNoSys, Locks locks) throws Exception { try( final FileToStreamTailer stdoutTailer = new FileToStreamTailer( @@ -160,7 +140,7 @@ int run(String serverDir, boolean setJnaNoSys, Locks locks) throws Exception { long retryStart = System.currentTimeMillis(); Socket ioSocket = null; Throwable socketThrowable = null; - while (ioSocket == null && System.currentTimeMillis() - retryStart < 1000) { + while (ioSocket == null && System.currentTimeMillis() - retryStart < serverInitWaitMillis) { try { ioSocket = AFUNIXSocket.connectTo(addr); } catch (Throwable e) { diff --git a/runner/client/src/mill/runner/client/MillClientMain.java b/runner/client/src/mill/runner/client/MillClientMain.java index 2d48cc4b507..d4c0b6913a3 100644 --- a/runner/client/src/mill/runner/client/MillClientMain.java +++ b/runner/client/src/mill/runner/client/MillClientMain.java @@ -41,9 +41,9 @@ public void initServer(String serverDir, boolean setJnaNoSys, Locks locks) throw MillProcessLauncher.launchMillServer(serverDir, setJnaNoSys); } }; - int exitCode = launcher.acquireLocksAndRun(OutFiles.out); + int exitCode = launcher.acquireLocksAndRun(OutFiles.out).exitCode; if (exitCode == Util.ExitServerCodeWhenVersionMismatch()) { - exitCode = launcher.acquireLocksAndRun(OutFiles.out); + exitCode = launcher.acquireLocksAndRun(OutFiles.out).exitCode; } System.exit(exitCode); } catch (ServerCouldNotBeStarted e) { From d99f74424f2ce43607d161981a40ed8fd600b1a7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 15 Aug 2024 00:41:19 -0700 Subject: [PATCH 28/34] . --- .../src/mill/main/client/ServerLauncher.java | 13 +++++- main/server/src/mill/main/server/Server.scala | 5 +- .../mill/main/server/ClientServerTests.scala | 46 ++++++++++++++----- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/main/client/src/mill/main/client/ServerLauncher.java b/main/client/src/mill/main/client/ServerLauncher.java index 8e47a367ef2..8c953f6f8d8 100644 --- a/main/client/src/mill/main/client/ServerLauncher.java +++ b/main/client/src/mill/main/client/ServerLauncher.java @@ -58,21 +58,26 @@ public static class Result{ Map env; String[] args; Locks[] memoryLocks; + int forceFailureForTestingMillisDelay; public ServerLauncher(InputStream stdin, PrintStream stdout, PrintStream stderr, Map env, String[] args, - Locks[] memoryLocks){ + Locks[] memoryLocks, + int forceFailureForTestingMillisDelay){ this.stdin = stdin; this.stdout = stdout; this.stderr = stderr; this.env = env; this.args = args; + // For testing in memory, we need to pass in the locks separately, so that the // locks can be shared between the different instances of `ServerLauncher` the // same way file locks are shared between different Mill client/secrer processes this.memoryLocks = memoryLocks; + + this.forceFailureForTestingMillisDelay = forceFailureForTestingMillisDelay; } public Result acquireLocksAndRun(String outDir) throws Exception { @@ -164,6 +169,12 @@ int run(String serverDir, boolean setJnaNoSys, Locks locks) throws Exception { outPumperThread.start(); inThread.start(); + System.out.println("forceFailureForTestingMillisDelay " + forceFailureForTestingMillisDelay); + if (forceFailureForTestingMillisDelay > 0){ + Thread.sleep(forceFailureForTestingMillisDelay); + System.out.println("forceFailureForTestingMillisDelay BOOM"); + throw new Exception("Force failure for testing: " + serverDir); + } outPumperThread.join(); try { diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index 0b5e68f409a..ad3909a7206 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -205,7 +205,10 @@ abstract class Server[T]( // two processes and gives a spurious deadlock error while (!done && !locks.clientLock.probe()) Thread.sleep(3) - if (!idle) exitServer() + if (!idle) { + serverLog("client interrupted while server was executing command") + exitServer() + } t.interrupt() // Try to give thread a moment to stop before we kill it for real diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 41b0ceb5fd2..2279680d6bc 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -72,7 +72,9 @@ object ClientServerTests extends TestSuite { val memoryLocks = Array.fill(10)(Locks.memory()); - def runClient(env: Map[String, String], args: Array[String]) = { + def apply(env: Map[String, String] = Map(), + args: Array[String] = Array(), + forceFailureForTestingMillisDelay: Int = -1) = { val in = new ByteArrayInputStream(s"hello$ENDL".getBytes()) val out = new ByteArrayOutputStream() val err = new ByteArrayOutputStream() @@ -82,7 +84,8 @@ object ClientServerTests extends TestSuite { new PrintStream(err), env.asJava, args, - memoryLocks + memoryLocks, + forceFailureForTestingMillisDelay ){ def initServer(serverDir: String, b: Boolean, locks: Locks) = { val serverId = "server-" + nextServerId @@ -100,7 +103,6 @@ object ClientServerTests extends TestSuite { ) } - def apply(env: Map[String, String], args: Array[String]) = runClient(env, args) } case class ClientResult(exitCode: Int, @@ -119,7 +121,7 @@ object ClientServerTests extends TestSuite { val tester = new Tester "hello" - { - val res1 = tester(Map(), Array("world")) + val res1 = tester(args = Array("world")) assert( res1.out == s"helloworld$ENDL", @@ -127,7 +129,7 @@ object ClientServerTests extends TestSuite { ) // A second client in sequence connect to the same server - val res2 = tester(Map(), Array(" WORLD")) + val res2 = tester(args = Array(" WORLD")) assert( res2.out == s"hello WORLD$ENDL", @@ -142,7 +144,7 @@ object ClientServerTests extends TestSuite { assert(res2.logsFor("exiting server") == Seq("server-0")) // Have a third client spawn/connect-to a new server at the same path - val res3 = tester(Map(), Array(" World")) + val res3 = tester(args = Array(" World")) assert( res3.out == s"hello World$ENDL", res3.err == s"HELLO World$ENDL" @@ -160,12 +162,11 @@ object ClientServerTests extends TestSuite { "concurrency" - { // Make sure concurrently running client commands results in multiple processes // being spawned, running in different folders - println("="*80) import concurrent._ import concurrent.ExecutionContext.Implicits.global - val f1 = Future(tester(Map(), Array(" World"))) - val f2 = Future(tester(Map(), Array(" WORLD"))) - val f3 = Future(tester(Map(), Array(" wOrLd"))) + val f1 = Future(tester(args = Array(" World"))) + val f2 = Future(tester(args = Array(" WORLD"))) + val f3 = Future(tester(args = Array(" wOrLd"))) val resF1 = Await.result(f1, duration.Duration.Inf) val resF2 = Await.result(f2, duration.Duration.Inf) val resF3 = Await.result(f3, duration.Duration.Inf) @@ -182,6 +183,27 @@ object ClientServerTests extends TestSuite { assert(resF3.out == s"hello wOrLd$ENDL") } + "clientLockReleasedOnFailure" - { + // When the client gets interrupted via Ctrl-C, we exit the server immediately. This + // is because Mill ends up executing arbitrary JVM code, and there is no generic way + // to interrupt such an execution. The two options are to leave the server running + // for an unbounded duration, or kill the server process and take a performance hit + // on the next cold startup. Mill chooses the second option. + import concurrent._ + import concurrent.ExecutionContext.Implicits.global + val res1 = intercept[Exception]{ + tester.apply(args = Array(" World"), forceFailureForTestingMillisDelay = 100) + } + + val s"Force failure for testing: $pathStr" = res1.getMessage + val logLines = os.read.lines(os.Path(pathStr, os.pwd) / "server.log") + + assert( + logLines.takeRight(2) == + Seq("server-0 client interrupted while server was executing command", "server-0 exiting server") + ) + } + "envVars" - retry(3) { // Make sure the simple "have the client start a server and // exchange one message" workflow works from end to end. @@ -197,7 +219,7 @@ object ClientServerTests extends TestSuite { "c" -> c1000 ) - val res1 = tester(env, Array()) + val res1 = tester(env = env) val expected = s"a=$a1000${ENDL}b=$b1000${ENDL}c=$c1000$ENDL" assert( @@ -228,7 +250,7 @@ object ClientServerTests extends TestSuite { ) val pathEnvVar = path.mkString(":") - val res2 = tester(Map("PATH" -> pathEnvVar), Array()) + val res2 = tester(env = Map("PATH" -> pathEnvVar)) val expected2 = s"PATH=$pathEnvVar$ENDL" From f68559ba1a5148965c407ba7b8cf8cb8151f498b Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 15 Aug 2024 21:00:03 +0800 Subject: [PATCH 29/34] . --- runner/client/src/mill/runner/client/MillClientMain.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runner/client/src/mill/runner/client/MillClientMain.java b/runner/client/src/mill/runner/client/MillClientMain.java index d4c0b6913a3..d22b87a1274 100644 --- a/runner/client/src/mill/runner/client/MillClientMain.java +++ b/runner/client/src/mill/runner/client/MillClientMain.java @@ -36,7 +36,7 @@ public static void main(String[] args) throws Exception { MillNoServerLauncher.runMain(args); } else try { // start in client-server mode - ServerLauncher launcher = new ServerLauncher(System.in, System.out, System.err, System.getenv(), args, null){ + ServerLauncher launcher = new ServerLauncher(System.in, System.out, System.err, System.getenv(), args, null, -1){ public void initServer(String serverDir, boolean setJnaNoSys, Locks locks) throws Exception{ MillProcessLauncher.launchMillServer(serverDir, setJnaNoSys); } From ac820795b3b17d2683829bcc337f38df325171b7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 15 Aug 2024 21:16:51 +0800 Subject: [PATCH 30/34] . --- main/server/src/mill/main/server/Server.scala | 72 ++++++++++--------- .../mill/main/server/ClientServerTests.scala | 52 ++++++++------ runner/src/mill/runner/MillServerMain.scala | 14 ++-- 3 files changed, 72 insertions(+), 66 deletions(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index ad3909a7206..c9d92992808 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -1,14 +1,12 @@ package mill.main.server -import sun.misc.{Signal, SignalHandler} - import java.io._ import java.net.Socket import scala.jdk.CollectionConverters._ import org.newsclub.net.unix.AFUNIXServerSocket import org.newsclub.net.unix.AFUNIXSocketAddress import mill.main.client._ -import mill.api.{SystemStreams, internal} +import mill.api.SystemStreams import mill.main.client.ProxyStream.Output import mill.main.client.lock.{Lock, Locks} @@ -31,7 +29,7 @@ abstract class Server[T]( var stateCache = stateCache0 def stateCache0: T - val serverId = java.lang.Long.toHexString(scala.util.Random.nextLong()) + val serverId: String = java.lang.Long.toHexString(scala.util.Random.nextLong()) def serverLog0(s: String): Unit = os.write.append(serverDir / ServerFiles.serverLog, s"$s\n", createFolders = true) @@ -42,27 +40,31 @@ abstract class Server[T]( val initialSystemProperties = sys.props.toMap try Server.tryLockBlock(locks.processLock) { - watchServerIdFile() - - while (running && { - val serverSocket = bindSocket() - try interruptWithTimeout(() => serverSocket.close(), () => serverSocket.accept()) match { - case None => false - case Some(sock) => - serverLog("handling run") - try handleRun(sock, initialSystemProperties) - catch { case e: Throwable => serverLog(e + "\n" + e.getStackTrace.mkString("\n")) } - finally sock.close(); - true + watchServerIdFile() + + while ( + running && { + val serverSocket = bindSocket() + try + interruptWithTimeout(() => serverSocket.close(), () => serverSocket.accept()) match { + case None => false + case Some(sock) => + serverLog("handling run") + try handleRun(sock, initialSystemProperties) + catch { + case e: Throwable => serverLog(e + "\n" + e.getStackTrace.mkString("\n")) + } finally sock.close(); + true + } + finally serverSocket.close() } - finally serverSocket.close() - }) () + ) () - }.getOrElse(throw new Exception("Mill server process already present")) + }.getOrElse(throw new Exception("Mill server process already present")) finally exitServer() } - def bindSocket() = { + def bindSocket(): AFUNIXServerSocket = { val socketPath = os.Path(ServerFiles.pipe(serverDir.toString())) os.remove.all(socketPath) @@ -84,26 +86,28 @@ abstract class Server[T]( pipedInput } - def watchServerIdFile() = { + def watchServerIdFile(): Unit = { os.write.over(serverDir / ServerFiles.serverId, serverId) val serverIdThread = new Thread( () => - while (running && { - Thread.sleep(100) - Try(os.read(serverDir / ServerFiles.serverId)).toOption match { - case None => - serverLog("serverId file missing") - exitServer() - false - case Some(s) => - if (s == serverId) true - else { - serverLog(s"serverId file contents $s does not match serverId $serverId") + while ( + running && { + Thread.sleep(100) + Try(os.read(serverDir / ServerFiles.serverId)).toOption match { + case None => + serverLog("serverId file missing") exitServer() false - } + case Some(s) => + if (s == serverId) true + else { + serverLog(s"serverId file contents $s does not match serverId $serverId") + exitServer() + false + } + } } - })() , + ) (), "Server ID Checker Thread" ) serverIdThread.start() diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 2279680d6bc..b36215f5a7b 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -9,7 +9,6 @@ import mill.api.SystemStreams import scala.jdk.CollectionConverters._ import utest._ - /** * Exercises the client-server logic in memory, using in-memory locks * and in-memory clients and servers @@ -18,7 +17,7 @@ object ClientServerTests extends TestSuite { val ENDL = System.lineSeparator() class EchoServer(override val serverId: String, serverDir: os.Path, locks: Locks) - extends Server[Option[Int]](serverDir, 1000, locks) with Runnable { + extends Server[Option[Int]](serverDir, 1000, locks) with Runnable { override def exitServer() = { serverLog("exiting server") super.exitServer() @@ -31,15 +30,15 @@ object ClientServerTests extends TestSuite { } def main0( - args: Array[String], - stateCache: Option[Int], - mainInteractive: Boolean, - streams: SystemStreams, - env: Map[String, String], - setIdle: Boolean => Unit, - systemProperties: Map[String, String], - initialSystemProperties: Map[String, String] - ) = { + args: Array[String], + stateCache: Option[Int], + mainInteractive: Boolean, + streams: SystemStreams, + env: Map[String, String], + setIdle: Boolean => Unit, + systemProperties: Map[String, String], + initialSystemProperties: Map[String, String] + ) = { val reader = new BufferedReader(new InputStreamReader(streams.in)) val str = reader.readLine() @@ -72,9 +71,11 @@ object ClientServerTests extends TestSuite { val memoryLocks = Array.fill(10)(Locks.memory()); - def apply(env: Map[String, String] = Map(), - args: Array[String] = Array(), - forceFailureForTestingMillisDelay: Int = -1) = { + def apply( + env: Map[String, String] = Map(), + args: Array[String] = Array(), + forceFailureForTestingMillisDelay: Int = -1 + ) = { val in = new ByteArrayInputStream(s"hello$ENDL".getBytes()) val out = new ByteArrayOutputStream() val err = new ByteArrayOutputStream() @@ -86,7 +87,7 @@ object ClientServerTests extends TestSuite { args, memoryLocks, forceFailureForTestingMillisDelay - ){ + ) { def initServer(serverDir: String, b: Boolean, locks: Locks) = { val serverId = "server-" + nextServerId nextServerId += 1 @@ -105,15 +106,17 @@ object ClientServerTests extends TestSuite { } - case class ClientResult(exitCode: Int, - serverDir: os.Path, - outDir: os.Path, - out: String, - err: String){ + case class ClientResult( + exitCode: Int, + serverDir: os.Path, + outDir: os.Path, + out: String, + err: String + ) { def logsFor(suffix: String) = { os.read .lines(serverDir / ServerFiles.serverLog) - .collect{case s if s.endsWith(" " + suffix) => s.dropRight(1 + suffix.length)} + .collect { case s if s.endsWith(" " + suffix) => s.dropRight(1 + suffix.length) } } } @@ -191,7 +194,7 @@ object ClientServerTests extends TestSuite { // on the next cold startup. Mill chooses the second option. import concurrent._ import concurrent.ExecutionContext.Implicits.global - val res1 = intercept[Exception]{ + val res1 = intercept[Exception] { tester.apply(args = Array(" World"), forceFailureForTestingMillisDelay = 100) } @@ -200,7 +203,10 @@ object ClientServerTests extends TestSuite { assert( logLines.takeRight(2) == - Seq("server-0 client interrupted while server was executing command", "server-0 exiting server") + Seq( + "server-0 client interrupted while server was executing command", + "server-0 exiting server" + ) ) } diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index 0779b2b222c..164a3eb3b0a 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -2,15 +2,9 @@ package mill.runner import sun.misc.{Signal, SignalHandler} -import java.io._ -import java.net.Socket -import scala.jdk.CollectionConverters._ -import org.newsclub.net.unix.AFUNIXServerSocket -import org.newsclub.net.unix.AFUNIXSocketAddress import mill.main.client._ -import mill.api.{SystemStreams, internal} -import mill.main.client.ProxyStream.Output -import mill.main.client.lock.{Lock, Locks} +import mill.api.SystemStreams +import mill.main.client.lock.Locks import scala.util.Try object MillServerMain { @@ -49,7 +43,9 @@ class MillServerMain( locks ) { - override def exitServer() = {super.exitServer(); System.exit(Util.ExitServerCodeWhenIdle())} + override def exitServer(): Unit = { + super.exitServer(); System.exit(Util.ExitServerCodeWhenIdle()) + } def stateCache0 = RunnerState.empty def main0( From 24cb3252dd1b8523b6c5d5d8a8e596e7d94b9c34 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 15 Aug 2024 21:28:42 +0800 Subject: [PATCH 31/34] . --- main/client/src/mill/main/client/ServerLauncher.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/main/client/src/mill/main/client/ServerLauncher.java b/main/client/src/mill/main/client/ServerLauncher.java index 8c953f6f8d8..58fbb2ee1e2 100644 --- a/main/client/src/mill/main/client/ServerLauncher.java +++ b/main/client/src/mill/main/client/ServerLauncher.java @@ -169,10 +169,8 @@ int run(String serverDir, boolean setJnaNoSys, Locks locks) throws Exception { outPumperThread.start(); inThread.start(); - System.out.println("forceFailureForTestingMillisDelay " + forceFailureForTestingMillisDelay); if (forceFailureForTestingMillisDelay > 0){ Thread.sleep(forceFailureForTestingMillisDelay); - System.out.println("forceFailureForTestingMillisDelay BOOM"); throw new Exception("Force failure for testing: " + serverDir); } outPumperThread.join(); From ef89ad5fb760673c12295f082bf663473775f7d2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 16 Aug 2024 08:48:14 +0800 Subject: [PATCH 32/34] . --- main/server/src/mill/main/server/Server.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index c9d92992808..c7c28be6417 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -30,8 +30,11 @@ abstract class Server[T]( def stateCache0: T val serverId: String = java.lang.Long.toHexString(scala.util.Random.nextLong()) - def serverLog0(s: String): Unit = - os.write.append(serverDir / ServerFiles.serverLog, s"$s\n", createFolders = true) + def serverLog0(s: String): Unit = { + if (os.exists(serverDir)) { + os.write.append(serverDir / ServerFiles.serverLog, s"$s\n", createFolders = true) + } + } def serverLog(s: String): Unit = serverLog0(s"$serverId $s") From a4e3496b503b30963cf8c69b13becaf6f7e6d911 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 16 Aug 2024 08:49:49 +0800 Subject: [PATCH 33/34] . --- main/server/src/mill/main/server/Server.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index c7c28be6417..84aeb63b07a 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -31,7 +31,7 @@ abstract class Server[T]( val serverId: String = java.lang.Long.toHexString(scala.util.Random.nextLong()) def serverLog0(s: String): Unit = { - if (os.exists(serverDir)) { + if (running && os.exists(serverDir)) { os.write.append(serverDir / ServerFiles.serverLog, s"$s\n", createFolders = true) } } From 06e32c11fc80f64e22928722b2dbf24b69213c47 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 16 Aug 2024 09:05:55 +0800 Subject: [PATCH 34/34] . --- main/server/src/mill/main/server/Server.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/server/src/mill/main/server/Server.scala b/main/server/src/mill/main/server/Server.scala index 84aeb63b07a..588b1533dac 100644 --- a/main/server/src/mill/main/server/Server.scala +++ b/main/server/src/mill/main/server/Server.scala @@ -31,7 +31,7 @@ abstract class Server[T]( val serverId: String = java.lang.Long.toHexString(scala.util.Random.nextLong()) def serverLog0(s: String): Unit = { - if (running && os.exists(serverDir)) { + if (running) { os.write.append(serverDir / ServerFiles.serverLog, s"$s\n", createFolders = true) } }