Skip to content

Commit 7457601

Browse files
authored
Fixes for runBackground mutex and log management (com-lihaoyi#3971)
Fixes com-lihaoyi#3955. * We make `runBackground` forward the stdout/stderr to `$serverDir/{stdout,stderr}` instead of `os.Inherit`. This is necessary because since we started using com-lihaoyi/os-lib@59b5fd9, `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 parent 4cddacd commit 7457601

File tree

15 files changed

+272
-216
lines changed

15 files changed

+272
-216
lines changed

example/scalalib/web/1-todo-webapp/test/src/WebAppTests.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import utest._
55
object WebAppTests extends TestSuite {
66
def withServer[T](example: cask.main.Main)(f: String => T): T = {
77
val server = io.undertow.Undertow.builder
8-
.addHttpListener(8081, "localhost")
8+
.addHttpListener(8181, "localhost")
99
.setHandler(example.defaultHandler)
1010
.build
1111
server.start()
1212
val res =
13-
try f("http://localhost:8081")
13+
try f("http://localhost:8181")
1414
finally server.stop()
1515
res
1616
}

example/scalalib/web/2-webapp-cache-busting/test/src/WebAppTests.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import utest._
55
object WebAppTests extends TestSuite {
66
def withServer[T](example: cask.main.Main)(f: String => T): T = {
77
val server = io.undertow.Undertow.builder
8-
.addHttpListener(8081, "localhost")
8+
.addHttpListener(8182, "localhost")
99
.setHandler(example.defaultHandler)
1010
.build
1111
server.start()
1212
val res =
13-
try f("http://localhost:8081")
13+
try f("http://localhost:8182")
1414
finally server.stop()
1515
res
1616
}

example/scalalib/web/4-webapp-scalajs/test/src/WebAppTests.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import utest._
55
object WebAppTests extends TestSuite {
66
def withServer[T](example: cask.main.Main)(f: String => T): T = {
77
val server = io.undertow.Undertow.builder
8-
.addHttpListener(8081, "localhost")
8+
.addHttpListener(8184, "localhost")
99
.setHandler(example.defaultHandler)
1010
.build
1111
server.start()
1212
val res =
13-
try f("http://localhost:8081")
13+
try f("http://localhost:8184")
1414
finally server.stop()
1515
res
1616
}

example/scalalib/web/5-webapp-scalajs-shared/test/src/WebAppTests.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import utest._
55
object WebAppTests extends TestSuite {
66
def withServer[T](example: cask.main.Main)(f: String => T): T = {
77
val server = io.undertow.Undertow.builder
8-
.addHttpListener(8081, "localhost")
8+
.addHttpListener(8185, "localhost")
99
.setHandler(example.defaultHandler)
1010
.build
1111
server.start()
1212
val res =
13-
try f("http://localhost:8081")
13+
try f("http://localhost:8185")
1414
finally server.stop()
1515
res
1616
}

main/client/src/mill/main/client/ServerLauncher.java

Lines changed: 50 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ public static class Result {
4747
public Path serverDir;
4848
}
4949

50-
static final int tailerRefreshIntervalMillis = 2;
5150
final int serverProcessesLimit = 5;
5251
final int serverInitWaitMillis = 10000;
5352

@@ -120,75 +119,63 @@ public Result acquireLocksAndRun(String outDir) throws Exception {
120119
}
121120

122121
int run(Path serverDir, boolean setJnaNoSys, Locks locks) throws Exception {
123-
try (final FileToStreamTailer stdoutTailer = new FileToStreamTailer(
124-
new java.io.File(serverDir + "/" + ServerFiles.stdout),
125-
stdout,
126-
tailerRefreshIntervalMillis);
127-
final FileToStreamTailer stderrTailer = new FileToStreamTailer(
128-
new java.io.File(serverDir + "/" + ServerFiles.stderr),
129-
stderr,
130-
tailerRefreshIntervalMillis); ) {
131-
stdoutTailer.start();
132-
stderrTailer.start();
133-
String serverPath = serverDir + "/" + ServerFiles.runArgs;
134-
try (OutputStream f = Files.newOutputStream(Paths.get(serverPath))) {
135-
f.write(System.console() != null ? 1 : 0);
136-
Util.writeString(f, BuildInfo.millVersion);
137-
Util.writeArgs(args, f);
138-
Util.writeMap(env, f);
139-
}
140-
141-
if (locks.processLock.probe()) {
142-
initServer(serverDir, setJnaNoSys, locks);
143-
}
144-
145-
while (locks.processLock.probe()) Thread.sleep(3);
146-
147-
String socketName = ServerFiles.pipe(serverDir.toString());
148-
AFUNIXSocketAddress addr = AFUNIXSocketAddress.of(new File(socketName));
122+
String serverPath = serverDir + "/" + ServerFiles.runArgs;
123+
try (OutputStream f = Files.newOutputStream(Paths.get(serverPath))) {
124+
f.write(System.console() != null ? 1 : 0);
125+
Util.writeString(f, BuildInfo.millVersion);
126+
Util.writeArgs(args, f);
127+
Util.writeMap(env, f);
128+
}
149129

150-
long retryStart = System.currentTimeMillis();
151-
Socket ioSocket = null;
152-
Throwable socketThrowable = null;
153-
while (ioSocket == null && System.currentTimeMillis() - retryStart < serverInitWaitMillis) {
154-
try {
155-
ioSocket = AFUNIXSocket.connectTo(addr);
156-
} catch (Throwable e) {
157-
socketThrowable = e;
158-
Thread.sleep(10);
159-
}
160-
}
130+
if (locks.processLock.probe()) {
131+
initServer(serverDir, setJnaNoSys, locks);
132+
}
161133

162-
if (ioSocket == null) {
163-
throw new Exception("Failed to connect to server", socketThrowable);
164-
}
134+
while (locks.processLock.probe()) Thread.sleep(3);
165135

166-
InputStream outErr = ioSocket.getInputStream();
167-
OutputStream in = ioSocket.getOutputStream();
168-
ProxyStream.Pumper outPumper = new ProxyStream.Pumper(outErr, stdout, stderr);
169-
InputPumper inPump = new InputPumper(() -> stdin, () -> in, true);
170-
Thread outPumperThread = new Thread(outPumper, "outPump");
171-
outPumperThread.setDaemon(true);
172-
Thread inThread = new Thread(inPump, "inPump");
173-
inThread.setDaemon(true);
174-
outPumperThread.start();
175-
inThread.start();
176-
177-
if (forceFailureForTestingMillisDelay > 0) {
178-
Thread.sleep(forceFailureForTestingMillisDelay);
179-
throw new Exception("Force failure for testing: " + serverDir);
180-
}
181-
outPumperThread.join();
136+
String socketName = ServerFiles.pipe(serverDir.toString());
137+
AFUNIXSocketAddress addr = AFUNIXSocketAddress.of(new File(socketName));
182138

139+
long retryStart = System.currentTimeMillis();
140+
Socket ioSocket = null;
141+
Throwable socketThrowable = null;
142+
while (ioSocket == null && System.currentTimeMillis() - retryStart < serverInitWaitMillis) {
183143
try {
184-
return Integer.parseInt(
185-
Files.readAllLines(Paths.get(serverDir + "/" + ServerFiles.exitCode))
186-
.get(0));
144+
ioSocket = AFUNIXSocket.connectTo(addr);
187145
} catch (Throwable e) {
188-
return Util.ExitClientCodeCannotReadFromExitCodeFile();
189-
} finally {
190-
ioSocket.close();
146+
socketThrowable = e;
147+
Thread.sleep(10);
191148
}
192149
}
150+
151+
if (ioSocket == null) {
152+
throw new Exception("Failed to connect to server", socketThrowable);
153+
}
154+
155+
InputStream outErr = ioSocket.getInputStream();
156+
OutputStream in = ioSocket.getOutputStream();
157+
ProxyStream.Pumper outPumper = new ProxyStream.Pumper(outErr, stdout, stderr);
158+
InputPumper inPump = new InputPumper(() -> stdin, () -> in, true);
159+
Thread outPumperThread = new Thread(outPumper, "outPump");
160+
outPumperThread.setDaemon(true);
161+
Thread inThread = new Thread(inPump, "inPump");
162+
inThread.setDaemon(true);
163+
outPumperThread.start();
164+
inThread.start();
165+
166+
if (forceFailureForTestingMillisDelay > 0) {
167+
Thread.sleep(forceFailureForTestingMillisDelay);
168+
throw new Exception("Force failure for testing: " + serverDir);
169+
}
170+
outPumperThread.join();
171+
172+
try {
173+
return Integer.parseInt(
174+
Files.readAllLines(Paths.get(serverDir + "/" + ServerFiles.exitCode)).get(0));
175+
} catch (Throwable e) {
176+
return Util.ExitClientCodeCannotReadFromExitCodeFile();
177+
} finally {
178+
ioSocket.close();
179+
}
193180
}
194181
}

main/util/src/mill/util/Jvm.scala

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package mill.util
22

33
import mill.api.Loose.Agg
44
import mill.api._
5+
import mill.main.client.ServerFiles
56
import os.{ProcessOutput, SubProcess}
67

78
import java.io._
@@ -117,8 +118,18 @@ object Jvm extends CoursierSupport {
117118
mainArgs,
118119
workingDir,
119120
if (!background) None
120-
else if (runBackgroundLogToConsole) Some((os.Inherit, os.Inherit))
121-
else Jvm.defaultBackgroundOutputs(ctx.dest),
121+
else if (runBackgroundLogToConsole) {
122+
val pwd0 = os.Path(java.nio.file.Paths.get(".").toAbsolutePath)
123+
// Hack to forward the background subprocess output to the Mill server process
124+
// stdout/stderr files, so the output will get properly slurped up by the Mill server
125+
// and shown to any connected Mill client even if the current command has completed
126+
Some(
127+
(
128+
os.PathAppendRedirect(pwd0 / ".." / ServerFiles.stdout),
129+
os.PathAppendRedirect(pwd0 / ".." / ServerFiles.stderr)
130+
)
131+
)
132+
} else Jvm.defaultBackgroundOutputs(ctx.dest),
122133
useCpPassingJar
123134
)
124135
}

mill

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
set -e
88

99
if [ -z "${DEFAULT_MILL_VERSION}" ] ; then
10-
DEFAULT_MILL_VERSION=0.11.12
10+
DEFAULT_MILL_VERSION=0.12.2
1111
fi
1212

1313
if [ -z "$MILL_VERSION" ] ; then

runner/src/mill/runner/MillBuildBootstrap.scala

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,6 @@ class MillBuildBootstrap(
7070
}
7171

7272
def evaluateRec(depth: Int): RunnerState = {
73-
mill.main.client.DebugLog.println(
74-
"MillBuildBootstrap.evaluateRec " + depth + " " + targetsAndParams.mkString(" ")
75-
)
7673
// println(s"+evaluateRec($depth) " + recRoot(projectRoot, depth))
7774
val prevFrameOpt = prevRunnerState.frames.lift(depth)
7875
val prevOuterFrameOpt = prevRunnerState.frames.lift(depth - 1)

runner/src/mill/runner/MillMain.scala

Lines changed: 67 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import java.io.{PipedInputStream, PrintStream}
44
import java.nio.file.Files
55
import java.nio.file.StandardOpenOption
66
import java.util.Locale
7-
import scala.jdk.CollectionConverters._
7+
import scala.jdk.CollectionConverters.*
88
import scala.util.Properties
99
import mill.java9rtexport.Export
1010
import mill.api.{MillException, SystemStreams, WorkspaceRoot, internal}
@@ -33,7 +33,7 @@ object MillMain {
3333
err.println(e.getCause.getMessage())
3434
(false, onError)
3535
case NonFatal(e) =>
36-
err.println("An unexpected error occurred")
36+
err.println("An unexpected error occurred " + e)
3737
throw e
3838
(false, onError)
3939
}
@@ -221,69 +221,74 @@ object MillMain {
221221
while (repeatForBsp) {
222222
repeatForBsp = false
223223

224-
val (isSuccess, evalStateOpt) = Watching.watchLoop(
225-
ringBell = config.ringBell.value,
226-
watch = config.watch.value,
227-
streams = streams,
228-
setIdle = setIdle,
229-
evaluate = (prevState: Option[RunnerState]) => {
230-
adjustJvmProperties(userSpecifiedProperties, initialSystemProperties)
231-
232-
withOutLock(
233-
config.noBuildLock.value || bspContext.isDefined,
234-
config.noWaitForBuildLock.value,
235-
out,
236-
targetsAndParams,
237-
streams
238-
) {
239-
val logger = getLogger(
240-
streams,
241-
config,
242-
mainInteractive,
243-
enableTicker =
244-
config.ticker
245-
.orElse(config.enableTicker)
246-
.orElse(Option.when(config.disableTicker.value)(false)),
247-
printLoggerState,
248-
serverDir,
249-
colored = colored,
250-
colors = colors
251-
)
252-
Using.resource(logger) { _ =>
253-
try new MillBuildBootstrap(
254-
projectRoot = WorkspaceRoot.workspaceRoot,
255-
output = out,
256-
home = config.home,
257-
keepGoing = config.keepGoing.value,
258-
imports = config.imports,
259-
env = env,
260-
threadCount = threadCount,
261-
targetsAndParams = targetsAndParams,
262-
prevRunnerState = prevState.getOrElse(stateCache),
263-
logger = logger,
264-
disableCallgraph = config.disableCallgraph.value,
265-
needBuildFile = needBuildFile(config),
266-
requestedMetaLevel = config.metaLevel,
267-
config.allowPositional.value,
268-
systemExit = systemExit,
269-
streams0 = streams0
270-
).evaluate()
224+
Using.resource(new TailManager(serverDir)) { tailManager =>
225+
val (isSuccess, evalStateOpt) = Watching.watchLoop(
226+
ringBell = config.ringBell.value,
227+
watch = config.watch.value,
228+
streams = streams,
229+
setIdle = setIdle,
230+
evaluate = (prevState: Option[RunnerState]) => {
231+
adjustJvmProperties(userSpecifiedProperties, initialSystemProperties)
232+
233+
withOutLock(
234+
config.noBuildLock.value || bspContext.isDefined,
235+
config.noWaitForBuildLock.value,
236+
out,
237+
targetsAndParams,
238+
streams
239+
) {
240+
Using.resource(getLogger(
241+
streams,
242+
config,
243+
mainInteractive,
244+
enableTicker =
245+
config.ticker
246+
.orElse(config.enableTicker)
247+
.orElse(Option.when(config.disableTicker.value)(false)),
248+
printLoggerState,
249+
serverDir,
250+
colored = colored,
251+
colors = colors
252+
)) { logger =>
253+
SystemStreams.withStreams(logger.systemStreams) {
254+
tailManager.withOutErr(logger.outputStream, logger.errorStream) {
255+
new MillBuildBootstrap(
256+
projectRoot = WorkspaceRoot.workspaceRoot,
257+
output = out,
258+
home = config.home,
259+
keepGoing = config.keepGoing.value,
260+
imports = config.imports,
261+
env = env,
262+
threadCount = threadCount,
263+
targetsAndParams = targetsAndParams,
264+
prevRunnerState = prevState.getOrElse(stateCache),
265+
logger = logger,
266+
disableCallgraph = config.disableCallgraph.value,
267+
needBuildFile = needBuildFile(config),
268+
requestedMetaLevel = config.metaLevel,
269+
config.allowPositional.value,
270+
systemExit = systemExit,
271+
streams0 = streams0
272+
).evaluate()
273+
}
274+
}
275+
}
271276
}
272-
}
273-
},
274-
colors = colors
275-
)
276-
bspContext.foreach { ctx =>
277-
repeatForBsp =
278-
BspContext.bspServerHandle.lastResult == Some(
279-
BspServerResult.ReloadWorkspace
280-
)
281-
streams.err.println(
282-
s"`$bspCmd` returned with ${BspContext.bspServerHandle.lastResult}"
277+
},
278+
colors = colors
283279
)
284-
}
280+
bspContext.foreach { ctx =>
281+
repeatForBsp =
282+
BspContext.bspServerHandle.lastResult == Some(
283+
BspServerResult.ReloadWorkspace
284+
)
285+
streams.err.println(
286+
s"`$bspCmd` returned with ${BspContext.bspServerHandle.lastResult}"
287+
)
288+
}
285289

286-
loopRes = (isSuccess, evalStateOpt)
290+
loopRes = (isSuccess, evalStateOpt)
291+
}
287292
} // while repeatForBsp
288293
bspContext.foreach { ctx =>
289294
streams.err.println(

0 commit comments

Comments
 (0)