From 7457601efdc5c959aef8bfd7bcb8aeb2513cdab3 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 16 Nov 2024 11:57:41 +0800 Subject: [PATCH] Fixes for `runBackground` mutex and log management (#3971) Fixes https://github.com/com-lihaoyi/mill/issues/3955. * We make `runBackground` forward the stdout/stderr to `$serverDir/{stdout,stderr}` instead of `os.Inherit`. This is necessary because since we started using https://github.com/com-lihaoyi/os-lib/commit/59b5fd93e56a3b86acbdb16d0c96e218fca6f320, `os.Inherit` is automatically pumped to the enclosing task logger, which for `runBackground` ends up being closed immediately so the logs are lost. * Now, the logs are instead picked up asynchronously by the `FileToStreamTailer` infrastructure, which picks them up and forwards them to any connected client regardless of who started the runBackground process * Moved usage of `FileToStreamTailer` from the mill client to the server. * This allows better integration with the Mill logging infrastructure, e.g. ensuring tailed logs properly interact with the multi-line prompt by clearing the prompt before being printed and re-printing the prompt after. * Simplified `BackgroundWrapper` * Renamed it `MillBackgroundWrapper` so it's more clear what it is when seen in `jps` * Use a file-lock for mutex, rather than polling on the process uuid/tombstone files * We still need to add a `Thread.sleep` after we take the lock, because the prior process seems to still hold on to sockets for some period of time. This defaults to 500ms (what is necessary experimentally) but is configurable by the new `runBackgroundRestartDelayMillis: T[Int]` task * Generally unified the creation/shutdown logic within `MillBackgroundWrapper`, rather than having it split between `BackgroundWrapper` and `def backgroundSetup` in the Mill server process Tested manually by running `rm -rf out && /Users/lihaoyi/Github/mill/target/mill-release -w runBackground` inside `example/javalib/web/1-hello-jetty`. Forced updates via `Enter` in the terminal or via editing server source files. Verified that the `runBackground` server logs appear in the console and that they do not conflict with the multi-line status prompt --- .../1-todo-webapp/test/src/WebAppTests.scala | 4 +- .../test/src/WebAppTests.scala | 4 +- .../test/src/WebAppTests.scala | 4 +- .../test/src/WebAppTests.scala | 4 +- .../src/mill/main/client/ServerLauncher.java | 113 +++++++-------- main/util/src/mill/util/Jvm.scala | 15 +- mill | 2 +- .../src/mill/runner/MillBuildBootstrap.scala | 3 - runner/src/mill/runner/MillMain.scala | 129 +++++++++--------- runner/src/mill/runner/MillServerMain.scala | 2 +- runner/src/mill/runner/TailManager.scala | 48 +++++++ .../backgroundwrapper/BackgroundWrapper.java | 38 ------ .../MillBackgroundWrapper.java | 60 ++++++++ scalalib/src/mill/scalalib/RunModule.scala | 60 ++++---- .../src/mill/testkit/IntegrationTester.scala | 2 +- 15 files changed, 272 insertions(+), 216 deletions(-) create mode 100644 runner/src/mill/runner/TailManager.scala delete mode 100644 scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/BackgroundWrapper.java create mode 100644 scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java diff --git a/example/scalalib/web/1-todo-webapp/test/src/WebAppTests.scala b/example/scalalib/web/1-todo-webapp/test/src/WebAppTests.scala index 2aa948961ab..c17e542f781 100644 --- a/example/scalalib/web/1-todo-webapp/test/src/WebAppTests.scala +++ b/example/scalalib/web/1-todo-webapp/test/src/WebAppTests.scala @@ -5,12 +5,12 @@ import utest._ object WebAppTests extends TestSuite { def withServer[T](example: cask.main.Main)(f: String => T): T = { val server = io.undertow.Undertow.builder - .addHttpListener(8081, "localhost") + .addHttpListener(8181, "localhost") .setHandler(example.defaultHandler) .build server.start() val res = - try f("http://localhost:8081") + try f("http://localhost:8181") finally server.stop() res } diff --git a/example/scalalib/web/2-webapp-cache-busting/test/src/WebAppTests.scala b/example/scalalib/web/2-webapp-cache-busting/test/src/WebAppTests.scala index 2aa948961ab..5122ca87a77 100644 --- a/example/scalalib/web/2-webapp-cache-busting/test/src/WebAppTests.scala +++ b/example/scalalib/web/2-webapp-cache-busting/test/src/WebAppTests.scala @@ -5,12 +5,12 @@ import utest._ object WebAppTests extends TestSuite { def withServer[T](example: cask.main.Main)(f: String => T): T = { val server = io.undertow.Undertow.builder - .addHttpListener(8081, "localhost") + .addHttpListener(8182, "localhost") .setHandler(example.defaultHandler) .build server.start() val res = - try f("http://localhost:8081") + try f("http://localhost:8182") finally server.stop() res } diff --git a/example/scalalib/web/4-webapp-scalajs/test/src/WebAppTests.scala b/example/scalalib/web/4-webapp-scalajs/test/src/WebAppTests.scala index 2aa948961ab..b60aa6562c0 100644 --- a/example/scalalib/web/4-webapp-scalajs/test/src/WebAppTests.scala +++ b/example/scalalib/web/4-webapp-scalajs/test/src/WebAppTests.scala @@ -5,12 +5,12 @@ import utest._ object WebAppTests extends TestSuite { def withServer[T](example: cask.main.Main)(f: String => T): T = { val server = io.undertow.Undertow.builder - .addHttpListener(8081, "localhost") + .addHttpListener(8184, "localhost") .setHandler(example.defaultHandler) .build server.start() val res = - try f("http://localhost:8081") + try f("http://localhost:8184") finally server.stop() res } diff --git a/example/scalalib/web/5-webapp-scalajs-shared/test/src/WebAppTests.scala b/example/scalalib/web/5-webapp-scalajs-shared/test/src/WebAppTests.scala index 2aa948961ab..459fd56bec0 100644 --- a/example/scalalib/web/5-webapp-scalajs-shared/test/src/WebAppTests.scala +++ b/example/scalalib/web/5-webapp-scalajs-shared/test/src/WebAppTests.scala @@ -5,12 +5,12 @@ import utest._ object WebAppTests extends TestSuite { def withServer[T](example: cask.main.Main)(f: String => T): T = { val server = io.undertow.Undertow.builder - .addHttpListener(8081, "localhost") + .addHttpListener(8185, "localhost") .setHandler(example.defaultHandler) .build server.start() val res = - try f("http://localhost:8081") + try f("http://localhost:8185") finally server.stop() res } diff --git a/main/client/src/mill/main/client/ServerLauncher.java b/main/client/src/mill/main/client/ServerLauncher.java index 3ce9a3c23a3..5bafa98690c 100644 --- a/main/client/src/mill/main/client/ServerLauncher.java +++ b/main/client/src/mill/main/client/ServerLauncher.java @@ -47,7 +47,6 @@ public static class Result { public Path serverDir; } - static final int tailerRefreshIntervalMillis = 2; final int serverProcessesLimit = 5; final int serverInitWaitMillis = 10000; @@ -120,75 +119,63 @@ public Result acquireLocksAndRun(String outDir) throws Exception { } int run(Path 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(); - String serverPath = serverDir + "/" + ServerFiles.runArgs; - try (OutputStream f = Files.newOutputStream(Paths.get(serverPath))) { - 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.toString()); - AFUNIXSocketAddress addr = AFUNIXSocketAddress.of(new File(socketName)); + String serverPath = serverDir + "/" + ServerFiles.runArgs; + try (OutputStream f = Files.newOutputStream(Paths.get(serverPath))) { + f.write(System.console() != null ? 1 : 0); + Util.writeString(f, BuildInfo.millVersion); + Util.writeArgs(args, f); + Util.writeMap(env, f); + } - long retryStart = System.currentTimeMillis(); - Socket ioSocket = null; - Throwable socketThrowable = null; - while (ioSocket == null && System.currentTimeMillis() - retryStart < serverInitWaitMillis) { - try { - ioSocket = AFUNIXSocket.connectTo(addr); - } catch (Throwable e) { - socketThrowable = e; - Thread.sleep(10); - } - } + if (locks.processLock.probe()) { + initServer(serverDir, setJnaNoSys, locks); + } - 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(); - - if (forceFailureForTestingMillisDelay > 0) { - Thread.sleep(forceFailureForTestingMillisDelay); - throw new Exception("Force failure for testing: " + serverDir); - } - outPumperThread.join(); + String socketName = ServerFiles.pipe(serverDir.toString()); + AFUNIXSocketAddress addr = AFUNIXSocketAddress.of(new File(socketName)); + long retryStart = System.currentTimeMillis(); + Socket ioSocket = null; + Throwable socketThrowable = null; + while (ioSocket == null && System.currentTimeMillis() - retryStart < serverInitWaitMillis) { try { - return Integer.parseInt( - Files.readAllLines(Paths.get(serverDir + "/" + ServerFiles.exitCode)) - .get(0)); + ioSocket = AFUNIXSocket.connectTo(addr); } catch (Throwable e) { - return Util.ExitClientCodeCannotReadFromExitCodeFile(); - } finally { - ioSocket.close(); + socketThrowable = e; + Thread.sleep(10); } } + + 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(); + + if (forceFailureForTestingMillisDelay > 0) { + Thread.sleep(forceFailureForTestingMillisDelay); + throw new Exception("Force failure for testing: " + serverDir); + } + 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/util/src/mill/util/Jvm.scala b/main/util/src/mill/util/Jvm.scala index 50e78931a0e..267d0566a8d 100644 --- a/main/util/src/mill/util/Jvm.scala +++ b/main/util/src/mill/util/Jvm.scala @@ -2,6 +2,7 @@ package mill.util import mill.api.Loose.Agg import mill.api._ +import mill.main.client.ServerFiles import os.{ProcessOutput, SubProcess} import java.io._ @@ -117,8 +118,18 @@ object Jvm extends CoursierSupport { mainArgs, workingDir, if (!background) None - else if (runBackgroundLogToConsole) Some((os.Inherit, os.Inherit)) - else Jvm.defaultBackgroundOutputs(ctx.dest), + else if (runBackgroundLogToConsole) { + val pwd0 = os.Path(java.nio.file.Paths.get(".").toAbsolutePath) + // Hack to forward the background subprocess output to the Mill server process + // stdout/stderr files, so the output will get properly slurped up by the Mill server + // and shown to any connected Mill client even if the current command has completed + Some( + ( + os.PathAppendRedirect(pwd0 / ".." / ServerFiles.stdout), + os.PathAppendRedirect(pwd0 / ".." / ServerFiles.stderr) + ) + ) + } else Jvm.defaultBackgroundOutputs(ctx.dest), useCpPassingJar ) } diff --git a/mill b/mill index d03a045cb77..84c22230cfe 100755 --- a/mill +++ b/mill @@ -7,7 +7,7 @@ set -e if [ -z "${DEFAULT_MILL_VERSION}" ] ; then - DEFAULT_MILL_VERSION=0.11.12 + DEFAULT_MILL_VERSION=0.12.2 fi if [ -z "$MILL_VERSION" ] ; then diff --git a/runner/src/mill/runner/MillBuildBootstrap.scala b/runner/src/mill/runner/MillBuildBootstrap.scala index dc3e0f99961..dc078a7b8e5 100644 --- a/runner/src/mill/runner/MillBuildBootstrap.scala +++ b/runner/src/mill/runner/MillBuildBootstrap.scala @@ -70,9 +70,6 @@ class MillBuildBootstrap( } def evaluateRec(depth: Int): RunnerState = { - mill.main.client.DebugLog.println( - "MillBuildBootstrap.evaluateRec " + depth + " " + targetsAndParams.mkString(" ") - ) // println(s"+evaluateRec($depth) " + recRoot(projectRoot, depth)) val prevFrameOpt = prevRunnerState.frames.lift(depth) val prevOuterFrameOpt = prevRunnerState.frames.lift(depth - 1) diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index 9a5dcf29056..580faa3283a 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -4,7 +4,7 @@ import java.io.{PipedInputStream, PrintStream} import java.nio.file.Files import java.nio.file.StandardOpenOption import java.util.Locale -import scala.jdk.CollectionConverters._ +import scala.jdk.CollectionConverters.* import scala.util.Properties import mill.java9rtexport.Export import mill.api.{MillException, SystemStreams, WorkspaceRoot, internal} @@ -33,7 +33,7 @@ object MillMain { err.println(e.getCause.getMessage()) (false, onError) case NonFatal(e) => - err.println("An unexpected error occurred") + err.println("An unexpected error occurred " + e) throw e (false, onError) } @@ -221,69 +221,74 @@ object MillMain { while (repeatForBsp) { repeatForBsp = false - val (isSuccess, evalStateOpt) = Watching.watchLoop( - ringBell = config.ringBell.value, - watch = config.watch.value, - streams = streams, - setIdle = setIdle, - evaluate = (prevState: Option[RunnerState]) => { - adjustJvmProperties(userSpecifiedProperties, initialSystemProperties) - - withOutLock( - config.noBuildLock.value || bspContext.isDefined, - config.noWaitForBuildLock.value, - out, - targetsAndParams, - streams - ) { - val logger = getLogger( - streams, - config, - mainInteractive, - enableTicker = - config.ticker - .orElse(config.enableTicker) - .orElse(Option.when(config.disableTicker.value)(false)), - printLoggerState, - serverDir, - colored = colored, - colors = colors - ) - Using.resource(logger) { _ => - try new MillBuildBootstrap( - projectRoot = WorkspaceRoot.workspaceRoot, - output = out, - home = config.home, - keepGoing = config.keepGoing.value, - imports = config.imports, - env = env, - threadCount = threadCount, - targetsAndParams = targetsAndParams, - prevRunnerState = prevState.getOrElse(stateCache), - logger = logger, - disableCallgraph = config.disableCallgraph.value, - needBuildFile = needBuildFile(config), - requestedMetaLevel = config.metaLevel, - config.allowPositional.value, - systemExit = systemExit, - streams0 = streams0 - ).evaluate() + Using.resource(new TailManager(serverDir)) { tailManager => + val (isSuccess, evalStateOpt) = Watching.watchLoop( + ringBell = config.ringBell.value, + watch = config.watch.value, + streams = streams, + setIdle = setIdle, + evaluate = (prevState: Option[RunnerState]) => { + adjustJvmProperties(userSpecifiedProperties, initialSystemProperties) + + withOutLock( + config.noBuildLock.value || bspContext.isDefined, + config.noWaitForBuildLock.value, + out, + targetsAndParams, + streams + ) { + Using.resource(getLogger( + streams, + config, + mainInteractive, + enableTicker = + config.ticker + .orElse(config.enableTicker) + .orElse(Option.when(config.disableTicker.value)(false)), + printLoggerState, + serverDir, + colored = colored, + colors = colors + )) { logger => + SystemStreams.withStreams(logger.systemStreams) { + tailManager.withOutErr(logger.outputStream, logger.errorStream) { + new MillBuildBootstrap( + projectRoot = WorkspaceRoot.workspaceRoot, + output = out, + home = config.home, + keepGoing = config.keepGoing.value, + imports = config.imports, + env = env, + threadCount = threadCount, + targetsAndParams = targetsAndParams, + prevRunnerState = prevState.getOrElse(stateCache), + logger = logger, + disableCallgraph = config.disableCallgraph.value, + needBuildFile = needBuildFile(config), + requestedMetaLevel = config.metaLevel, + config.allowPositional.value, + systemExit = systemExit, + streams0 = streams0 + ).evaluate() + } + } + } } - } - }, - colors = colors - ) - bspContext.foreach { ctx => - repeatForBsp = - BspContext.bspServerHandle.lastResult == Some( - BspServerResult.ReloadWorkspace - ) - streams.err.println( - s"`$bspCmd` returned with ${BspContext.bspServerHandle.lastResult}" + }, + colors = colors ) - } + bspContext.foreach { ctx => + repeatForBsp = + BspContext.bspServerHandle.lastResult == Some( + BspServerResult.ReloadWorkspace + ) + streams.err.println( + s"`$bspCmd` returned with ${BspContext.bspServerHandle.lastResult}" + ) + } - loopRes = (isSuccess, evalStateOpt) + loopRes = (isSuccess, evalStateOpt) + } } // while repeatForBsp bspContext.foreach { ctx => streams.err.println( diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index 917a2f3b619..b426af94dd4 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -24,7 +24,7 @@ object MillServerMain { ) val acceptTimeoutMillis = - Try(System.getProperty("mill.server_timeout").toInt).getOrElse(5 * 60 * 1000) // 5 minutes + Try(System.getProperty("mill.server_timeout").toInt).getOrElse(30 * 60 * 1000) // 30 minutes new MillServerMain( serverDir = os.Path(args0(0)), diff --git a/runner/src/mill/runner/TailManager.scala b/runner/src/mill/runner/TailManager.scala new file mode 100644 index 00000000000..9290fd07cf4 --- /dev/null +++ b/runner/src/mill/runner/TailManager.scala @@ -0,0 +1,48 @@ +package mill.runner + +import mill.api.SystemStreams.ThreadLocalStreams +import mill.main.client.{FileToStreamTailer, ServerFiles} + +import java.io.{OutputStream, PrintStream} + +class TailManager(serverDir: os.Path) extends AutoCloseable { + val tailerRefreshIntervalMillis = 2 + + // We need to explicitly manage tailerOut/tailerErr ourselves, rather than relying + // on System.out/System.err redirects, because those redirects are ThreadLocal and + // do not affect the tailers which run on their own separate threads + @volatile var tailerOut: OutputStream = System.out + @volatile var tailerErr: OutputStream = System.err + val stdoutTailer = new FileToStreamTailer( + (serverDir / ServerFiles.stdout).toIO, + new PrintStream(new ThreadLocalStreams.ProxyOutputStream { + def delegate(): OutputStream = tailerOut + }), + tailerRefreshIntervalMillis + ) + val stderrTailer = new FileToStreamTailer( + (serverDir / ServerFiles.stderr).toIO, + new PrintStream(new ThreadLocalStreams.ProxyOutputStream { + def delegate(): OutputStream = tailerErr + }), + tailerRefreshIntervalMillis + ) + + stdoutTailer.start() + stderrTailer.start() + + def withOutErr[T](newOut: OutputStream, newErr: OutputStream)(t: => T): T = { + tailerOut = newOut + tailerErr = newErr + try t + finally { + tailerOut = System.out + tailerErr = System.err + } + } + + override def close(): Unit = { + stdoutTailer.close() + stderrTailer.close() + } +} diff --git a/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/BackgroundWrapper.java b/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/BackgroundWrapper.java deleted file mode 100644 index dbbcff8d34c..00000000000 --- a/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/BackgroundWrapper.java +++ /dev/null @@ -1,38 +0,0 @@ -package mill.scalalib.backgroundwrapper; - -public class BackgroundWrapper { - public static void main(String[] args) throws Exception { - String watched = args[0]; - String tombstone = args[1]; - String expected = args[2]; - Thread watcher = new Thread(new Runnable() { - @Override - public void run() { - while (true) { - try { - Thread.sleep(50); - String token = java.nio.file.Files.readString(java.nio.file.Paths.get(watched)); - if (!token.equals(expected)) { - new java.io.File(tombstone).createNewFile(); - System.exit(0); - } - } catch (Exception e) { - try { - new java.io.File(tombstone).createNewFile(); - } catch (Exception e2) { - } - System.exit(0); - } - } - } - }); - watcher.setDaemon(true); - watcher.start(); - String realMain = args[3]; - String[] realArgs = new String[args.length - 4]; - for (int i = 0; i < args.length - 4; i++) { - realArgs[i] = args[i + 4]; - } - Class.forName(realMain).getMethod("main", String[].class).invoke(null, (Object) realArgs); - } -} diff --git a/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java b/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java new file mode 100644 index 00000000000..ebdd96f84d9 --- /dev/null +++ b/scalalib/backgroundwrapper/src/mill/scalalib/backgroundwrapper/MillBackgroundWrapper.java @@ -0,0 +1,60 @@ +package mill.scalalib.backgroundwrapper; + +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +public class MillBackgroundWrapper { + public static void main(String[] args) throws Exception { + Path procUuidPath = Paths.get(args[0]); + Path procLockfile = Paths.get(args[1]); + String procUuid = args[2]; + int lockDelay = Integer.parseInt(args[3]); + + Files.writeString(procUuidPath, procUuid, StandardOpenOption.CREATE); + + // Take a lock on `procLockfile` to ensure that only one + // `runBackground` process is running at any point in time. + RandomAccessFile raf = new RandomAccessFile(procLockfile.toFile(), "rw"); + FileChannel chan = raf.getChannel(); + if (chan.tryLock() == null) { + System.err.println("Waiting for runBackground lock to be available"); + chan.lock(); + } + + // For some reason even after the previous process exits things like sockets + // may still take time to free, so sleep for a configurable duration before proceeding + Thread.sleep(lockDelay); + + // Start the thread to watch for updates on the process marker file, + // so we can exit if it is deleted or replaced + long startTime = System.currentTimeMillis(); + Thread watcher = new Thread(() -> { + while (true) { + long delta = (System.currentTimeMillis() - startTime) / 1000; + try { + Thread.sleep(1); + String token = Files.readString(procUuidPath); + if (!token.equals(procUuid)) { + System.err.println("runBackground exiting after " + delta + "s"); + System.exit(0); + } + } catch (Exception e) { + System.err.println("runBackground exiting after " + delta + "s"); + System.exit(0); + } + } + }); + + watcher.setDaemon(true); + watcher.start(); + + // Actually start the Java main method we wanted to run in the background + String realMain = args[4]; + String[] realArgs = java.util.Arrays.copyOfRange(args, 5, args.length); + Class.forName(realMain).getMethod("main", String[].class).invoke(null, (Object) realArgs); + } +} diff --git a/scalalib/src/mill/scalalib/RunModule.scala b/scalalib/src/mill/scalalib/RunModule.scala index 4742a01a864..02b603985db 100644 --- a/scalalib/src/mill/scalalib/RunModule.scala +++ b/scalalib/src/mill/scalalib/RunModule.scala @@ -150,10 +150,16 @@ trait RunModule extends WithZincWorker { def runBackgroundTask(mainClass: Task[String], args: Task[Args] = Task.Anon(Args())): Task[Unit] = Task.Anon { - val (procId, procTombstone, token) = backgroundSetup(T.dest) + val (procUuidPath, procLockfile, procUuid) = backgroundSetup(T.dest) runner().run( - args = Seq(procId.toString, procTombstone.toString, token, mainClass()) ++ args().value, - mainClass = "mill.scalalib.backgroundwrapper.BackgroundWrapper", + args = Seq( + procUuidPath.toString, + procLockfile.toString, + procUuid, + runBackgroundRestartDelayMillis().toString, + mainClass() + ) ++ args().value, + mainClass = "mill.scalalib.backgroundwrapper.MillBackgroundWrapper", workingDir = forkWorkingDir(), extraRunClasspath = zincWorker().backgroundWrapperClasspath().map(_.path).toSeq, background = true, @@ -171,6 +177,7 @@ trait RunModule extends WithZincWorker { */ // TODO: make this a task, to be more dynamic def runBackgroundLogToConsole: Boolean = true + def runBackgroundRestartDelayMillis: T[Int] = 500 @deprecated("Binary compat shim, use `.runner().run(..., background=true)`", "Mill 0.12.0") protected def doRunBackground( @@ -184,14 +191,20 @@ trait RunModule extends WithZincWorker { runUseArgsFile: Boolean, backgroundOutputs: Option[Tuple2[ProcessOutput, ProcessOutput]] )(args: String*): Ctx => Result[Unit] = ctx => { - val (procId, procTombstone, token) = backgroundSetup(taskDest) + val (procUuidPath, procLockfile, procUuid) = backgroundSetup(taskDest) try Result.Success( Jvm.runSubprocessWithBackgroundOutputs( - "mill.scalalib.backgroundwrapper.BackgroundWrapper", + "mill.scalalib.backgroundwrapper.MillBackgroundWrapper", (runClasspath ++ zwBackgroundWrapperClasspath).map(_.path), forkArgs, forkEnv, - Seq(procId.toString, procTombstone.toString, token, finalMainClass) ++ args, + Seq( + procUuidPath.toString, + procLockfile.toString, + procUuid, + 500.toString, + finalMainClass + ) ++ args, workingDir = forkWorkingDir, backgroundOutputs, useCpPassingJar = runUseArgsFile @@ -204,38 +217,11 @@ trait RunModule extends WithZincWorker { } private[this] def backgroundSetup(dest: os.Path): (Path, Path, String) = { - val token = java.util.UUID.randomUUID().toString - val procId = dest / ".mill-background-process-id" - val procTombstone = dest / ".mill-background-process-tombstone" - // The background subprocesses poll the procId file, and kill themselves - // when the procId file is deleted. This deletion happens immediately before - // the body of these commands run, but we cannot be sure the subprocess has - // had time to notice. - // - // To make sure we wait for the previous subprocess to - // die, we make the subprocess write a tombstone file out when it kills - // itself due to procId being deleted, and we wait a short time on task-start - // to see if such a tombstone appears. If a tombstone appears, we can be sure - // the subprocess has killed itself, and can continue. If a tombstone doesn't - // appear in a short amount of time, we assume the subprocess exited or was - // killed via some other means, and continue anyway. - val start = System.currentTimeMillis() - while ({ - if (os.exists(procTombstone)) { - Thread.sleep(10) - os.remove.all(procTombstone) - true - } else { - Thread.sleep(10) - System.currentTimeMillis() - start < 100 - } - }) () - - os.write(procId, token) - os.write(procTombstone, token) - (procId, procTombstone, token) + val procUuid = java.util.UUID.randomUUID().toString + val procUuidPath = dest / ".mill-background-process-uuid" + val procLockfile = dest / ".mill-background-process-lock" + (procUuidPath, procLockfile, procUuid) } - } object RunModule { diff --git a/testkit/src/mill/testkit/IntegrationTester.scala b/testkit/src/mill/testkit/IntegrationTester.scala index c6c3488de22..50e3fdefc65 100644 --- a/testkit/src/mill/testkit/IntegrationTester.scala +++ b/testkit/src/mill/testkit/IntegrationTester.scala @@ -144,7 +144,7 @@ object IntegrationTester { if (clientServerMode) { // try to stop the server os.call( - cmd = (millExecutable, "shutdown"), + cmd = (millExecutable, "--no-build-lock", "shutdown"), cwd = workspacePath, stdin = os.Inherit, stdout = os.Inherit,