Skip to content

Commit 91e07e1

Browse files
Prefix logging for BSP (#5180)
Same as #5154, with a cleaner git history to ease review --- When Mill runs as a BSP server, this makes it log in a similar way as when running Mill from the command-line, with `[…] ` prefixes. For example, the small BSP session run in the test added in this PR logs the following to stderr: ```text [bsp] Trying to load BSP server... [bsp] BSP server started [1-buildInitialize] Entered buildInitialize [1-buildInitialize] buildInitialize took 2 msec [2-workspaceBuildTargets] Entered workspaceBuildTargets [bsp-init-build.mill-61] [info] compiling 3 Scala sources to /Users/alexandre/projects/mill/mill-class-loading/out/integration/ide/bsp-server/local/daemon/testForked.dest/sandbox/run-1/out/mill-build/compile.dest/classes ... [bsp-init-build.mill-61] [warn] package scala contains object and package with same name: caps. [bsp-init-build.mill-61] [warn] This indicates that there are several versions of the Scala standard library on the classpath. [bsp-init-build.mill-61] [warn] The build should be reconfigured so that only one version of the standard library is on the classpath. [bsp-init-build.mill-61] [warn] one warning found [bsp-init-build.mill-61] [info] done compiling [bsp-init] SNAPSHOT [2-workspaceBuildTargets] Evaluating 5 tasks [2-workspaceBuildTargets] Done [2-workspaceBuildTargets] Evaluating 1 task [2-workspaceBuildTargets] Done [2-workspaceBuildTargets] workspaceBuildTargets took 4197 msec [3-loggingTest] Entered loggingTest [3-loggingTest] Evaluating 1 task [3-loggingTest-1] bspLoggingTest from System.out [3-loggingTest-1] bspLoggingTest from System.err [3-loggingTest-1] bspLoggingTest from Console.out [3-loggingTest-1] bspLoggingTest from Console.err [3-loggingTest-1] bspLoggingTest from Task.log.info [3-loggingTest] Done [3-loggingTest] loggingTest took 11 msec [4-buildShutdown] Entered buildShutdown [5-onBuildExit] Entered onBuildExit Reload finished, result: mill.api.internal.BspServerResult$Shutdown$@6726cc69 Shutting down executor [bsp] BSP session returned with mill.api.internal.BspServerResult$Shutdown$@6726cc69 Stopping server via handle... [bsp] Exiting BSP runner loop. Stopping BSP server. Last result: Success(mill.api.internal.BspServerResult$Shutdown$@6726cc69) ``` This helps make sense of what's going on when working on BSP-releated features, but could also help Mill BSP users in case anything goes wrong.
1 parent 787a791 commit 91e07e1

File tree

22 files changed

+590
-174
lines changed

22 files changed

+590
-174
lines changed

core/api/src/mill/api/SystemStreams.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package mill.api
33
import java.io.InputStream
44
import java.io.PrintStream
55

6+
import scala.util.DynamicVariable
7+
68
/**
79
* Represents a set of streams that look similar to those provided by the
810
* operating system. These may internally be proxied/redirected/processed, but
@@ -14,3 +16,8 @@ class SystemStreams(
1416
val err: PrintStream,
1517
val in: InputStream
1618
)
19+
20+
object SystemStreams {
21+
val original = new SystemStreams(System.out, System.err, System.in)
22+
val current = new DynamicVariable(original)
23+
}

core/api/src/mill/api/internal/api.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ trait JavaModuleApi extends ModuleApi {
3232

3333
private[mill] def bspBuildTargetCompile: TaskApi[java.nio.file.Path]
3434

35+
private[mill] def bspLoggingTest: TaskApi[Unit]
36+
3537
private[mill] def bspBuildTargetJavacOptions(clientWantsSemanticDb: Boolean)
3638
: TaskApi[EvaluatorApi => (java.nio.file.Path, Seq[String], Seq[String])]
3739

core/define/src/mill/define/SystemStreams.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import mill.api.DummyInputStream
44
import mill.constants.InputPumper
55

66
import java.io.{InputStream, OutputStream, PrintStream}
7-
import scala.util.DynamicVariable
87

98
object SystemStreams {
109

@@ -16,7 +15,7 @@ object SystemStreams {
1615
* That means that the logs may appear out of order, jumbling your logs and screwing up
1716
* your terminal
1817
*/
19-
val original = new mill.api.SystemStreams(System.out, System.err, System.in)
18+
def original = mill.api.SystemStreams.original
2019

2120
/**
2221
* Used to check whether the system streams are all "original", i,e. they
@@ -116,6 +115,7 @@ object SystemStreams {
116115
}
117116
}
118117
def setTopLevelSystemStreamProxy(): Unit = {
118+
val _ = mill.api.SystemStreams.current
119119
// Make sure to initialize `Console` to cache references to the original
120120
// `System.{in,out,err}` streams before we redirect them
121121
val _ = Console.out
@@ -126,8 +126,11 @@ object SystemStreams {
126126
System.setErr(ThreadLocalStreams.Err)
127127
}
128128

129+
def current(): mill.api.SystemStreams =
130+
ThreadLocalStreams.current.value
131+
129132
private[mill] object ThreadLocalStreams {
130-
val current = new DynamicVariable(original)
133+
def current = mill.api.SystemStreams.current
131134

132135
object Out extends PrintStream(new ProxyOutputStream { def delegate() = current.value.out })
133136
object Err extends PrintStream(new ProxyOutputStream { def delegate() = current.value.err })

core/internal/src/mill/internal/PrefixLogger.scala

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,25 +56,30 @@ private[mill] class PrefixLogger(
5656
logger0.unprefixedStreams.in
5757
)
5858

59+
private def addPrefix(prefix: String, s: String): String =
60+
s.linesWithSeparators.map(prefix + _).mkString
61+
5962
override def info(s: String): Unit = {
6063
prompt.reportKey(logKey)
61-
logger0.info("" + prompt.infoColor(linePrefix) + s)
64+
unprefixedStreams.err.println(addPrefix(prompt.infoColor(linePrefix), s))
6265
}
6366
override def warn(s: String): Unit = {
6467
prompt.reportKey(logKey)
65-
logger0.warn("" + prompt.warnColor(linePrefix) + s)
68+
unprefixedStreams.err.println(addPrefix(prompt.warnColor(linePrefix), s))
6669
}
6770
override def error(s: String): Unit = {
6871
prompt.reportKey(logKey)
69-
logger0.error("" + prompt.infoColor(linePrefix) + s)
72+
unprefixedStreams.err.println(addPrefix(prompt.errorColor(linePrefix), s))
7073
}
7174
override def ticker(s: String): Unit = prompt.setPromptDetail(logKey, s)
7275

7376
def prompt = logger0.prompt
7477

7578
override def debug(s: String): Unit = {
76-
if (prompt.debugEnabled) prompt.reportKey(logKey)
77-
logger0.debug("" + prompt.infoColor(linePrefix) + s)
79+
if (debugEnabled) {
80+
if (prompt.debugEnabled) prompt.reportKey(logKey)
81+
unprefixedStreams.err.println(addPrefix(prompt.infoColor(linePrefix), s))
82+
}
7883
}
7984
override def withOutStream(outStream: PrintStream): Logger = new ProxyLogger(this) with Logger {
8085
override lazy val unprefixedStreams = new SystemStreams(
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package mill.internal
2+
3+
import mill.api.{SystemStreams, Logger}
4+
5+
import java.io.PrintStream
6+
7+
private[mill] class SimpleLogger(
8+
override val unprefixedStreams: SystemStreams,
9+
override val logKey: Seq[String],
10+
debugEnabled: Boolean
11+
) extends Logger {
12+
override def toString: String = s"SimpleLogger($unprefixedStreams, $debugEnabled)"
13+
14+
private val linePrefix: String =
15+
if (logKey.isEmpty) ""
16+
else s"[${logKey.mkString("-")}] "
17+
private def prefixPrintStream(stream: java.io.OutputStream) = {
18+
new PrintStream(new LinePrefixOutputStream(
19+
linePrefix,
20+
stream,
21+
() => ()
22+
))
23+
}
24+
25+
val streams = new SystemStreams(
26+
out = prefixPrintStream(unprefixedStreams.out),
27+
err = prefixPrintStream(unprefixedStreams.err),
28+
unprefixedStreams.in
29+
)
30+
31+
def isInteractive() = false
32+
33+
def info(s: String): Unit =
34+
unprefixedStreams.err.println(s)
35+
36+
def warn(s: String): Unit =
37+
unprefixedStreams.err.println(s)
38+
39+
def error(s: String): Unit =
40+
unprefixedStreams.err.println(s)
41+
42+
val prompt = new Logger.Prompt.NoOp {
43+
override def enableTicker = true
44+
}
45+
def ticker(s: String): Unit = ()
46+
47+
def debug(s: String): Unit =
48+
if (debugEnabled)
49+
unprefixedStreams.err.println(s)
50+
}

integration/ide/bsp-server/resources/project/build.mill

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,13 @@ object app extends scalalib.JavaModule {
3737
mvn"com.mysql:mysql-connector-j:9.1.0"
3838
)
3939
}
40+
41+
object errored extends scalalib.ScalaModule {
42+
def scalaVersion = Option(System.getenv("TEST_SCALA_2_13_VERSION")).getOrElse(???)
43+
44+
def compile = Task {
45+
val res = super.compile()
46+
sys.error("nope")
47+
res
48+
}
49+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[bsp] Trying to load BSP server...
2+
[bsp] BSP server started
3+
[1-buildInitialize] Entered buildInitialize
4+
[1-buildInitialize] buildInitialize took * msec
5+
[2-workspaceBuildTargets] Entered workspaceBuildTargets
6+
[bsp-init-build.mill-61] [info] compiling * Scala sources to * ...
7+
[bsp-init-build.mill-61] [info] done compiling
8+
[bsp-init] SNAPSHOT
9+
[2-workspaceBuildTargets] Evaluating * tasks
10+
[2-workspaceBuildTargets] Done
11+
[2-workspaceBuildTargets] Evaluating 1 task
12+
[2-workspaceBuildTargets] Done
13+
[2-workspaceBuildTargets] workspaceBuildTargets took * msec
14+
[3-loggingTest] Entered loggingTest
15+
[3-loggingTest] Evaluating 1 task
16+
[3-loggingTest-1] bspLoggingTest from System.out
17+
[3-loggingTest-1] bspLoggingTest from System.err
18+
[3-loggingTest-1] bspLoggingTest from Console.out
19+
[3-loggingTest-1] bspLoggingTest from Console.err
20+
[3-loggingTest-1] bspLoggingTest from Task.log.info
21+
[3-loggingTest] Done
22+
[3-loggingTest] loggingTest took * msec
23+
[4-compile] Entered buildTargetCompile
24+
[4-compile] Evaluating 1 task
25+
[4-compile] * tasks failed
26+
[4-compile] errored.compile java.lang.RuntimeException: nope
27+
[4-compile] scala.sys.package$.error(package.scala:*)
28+
[4-compile] build_.package_.build_$package_$errored$$$_$compile$$anonfun$1$$anonfun$1(build.mill:*)
29+
[4-compile] mill.define.Task$Named.evaluate(Task.scala:*)
30+
[4-compile] mill.define.Task$Named.evaluate$(Task.scala:*)
31+
[4-compile] mill.define.Task$Computed.evaluate(Task.scala:*)
32+
[4-compile] Failed
33+
[4-compile] buildTargetCompile took * msec
34+
[5-buildShutdown] Entered buildShutdown
35+
[6-onBuildExit] Entered onBuildExit
36+
[bsp] BSP session returned with mill.api.internal.BspServerResult$Shutdown$@*
37+
[bsp] Exiting BSP runner loop. Stopping BSP server. Last result: Success(mill.api.internal.BspServerResult$Shutdown$@*)

integration/ide/bsp-server/resources/snapshots/workspace-build-targets.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,46 @@
3030
"javaVersion": "<java-version>"
3131
}
3232
},
33+
{
34+
"id": {
35+
"uri": "file:///workspace/errored"
36+
},
37+
"displayName": "errored",
38+
"baseDirectory": "file:///workspace/errored",
39+
"tags": [
40+
"library",
41+
"application"
42+
],
43+
"languageIds": [
44+
"java",
45+
"scala"
46+
],
47+
"dependencies": [],
48+
"capabilities": {
49+
"canCompile": true,
50+
"canTest": false,
51+
"canRun": true,
52+
"canDebug": false
53+
},
54+
"dataKind": "scala",
55+
"data": {
56+
"scalaOrganization": "org.scala-lang",
57+
"scalaVersion": "<scala-version>",
58+
"scalaBinaryVersion": "2.13",
59+
"platform": 1,
60+
"jars": [
61+
"file:///coursier-cache/https/repo1.maven.org/maven2/org/scala-lang/scala-compiler/<scala-version>/scala-compiler-<scala-version>.jar",
62+
"file:///coursier-cache/https/repo1.maven.org/maven2/org/scala-lang/scala-reflect/<scala-version>/scala-reflect-<scala-version>.jar",
63+
"file:///coursier-cache/https/repo1.maven.org/maven2/org/scala-lang/scala-library/<scala-version>/scala-library-<scala-version>.jar",
64+
"file:///coursier-cache/https/repo1.maven.org/maven2/io/github/java-diff-utils/java-diff-utils/<java-diff-utils-version>/java-diff-utils-<java-diff-utils-version>.jar",
65+
"file:///coursier-cache/https/repo1.maven.org/maven2/org/jline/jline/<jline-version>/jline-<jline-version>-jdk8.jar"
66+
],
67+
"jvmBuildTarget": {
68+
"javaHome": "java-home",
69+
"javaVersion": "<java-version>"
70+
}
71+
}
72+
},
3373
{
3474
"id": {
3575
"uri": "file:///workspace/hello-java"

integration/ide/bsp-server/src/BspServerTestUtil.scala

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import com.google.gson.{Gson, GsonBuilder}
55
import coursier.cache.CacheDefaults
66
import mill.api.BuildInfo
77
import mill.bsp.Constants
8+
import mill.testrunner.TestRunnerUtils
89
import org.eclipse.{lsp4j => l}
10+
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
911

1012
import java.io.ByteArrayOutputStream
1113
import java.util.concurrent.atomic.AtomicInteger
12-
import java.util.concurrent.{ExecutorService, Executors, ThreadFactory}
14+
import java.util.concurrent.{CompletableFuture, ExecutorService, Executors, ThreadFactory}
1315

1416
import scala.jdk.CollectionConverters._
1517
import scala.reflect.ClassTag
@@ -96,6 +98,50 @@ object BspServerTestUtil {
9698
}
9799
}
98100

101+
def compareLogWithSnapshot(
102+
log: String,
103+
snapshotPath: os.Path,
104+
ignoreLine: String => Boolean = _ => false
105+
): Unit = {
106+
107+
val logLines = log.linesIterator.filterNot(ignoreLine).toVector
108+
val snapshotLinesOpt = Option.when(os.exists(snapshotPath))(os.read.lines(snapshotPath))
109+
110+
val matches = snapshotLinesOpt match {
111+
case Some(snapshotLines) =>
112+
if (snapshotLines.length == logLines.length)
113+
snapshotLines.iterator
114+
.zip(logLines.iterator)
115+
.zipWithIndex
116+
.map {
117+
case ((snapshotLine, logLine), lineIdx) =>
118+
val cmp = TestRunnerUtils.matchesGlob(snapshotLine)
119+
cmp(logLine) || {
120+
System.err.println(s"Line ${lineIdx + 1} differs:")
121+
System.err.println(s" Expected: $snapshotLine")
122+
System.err.println(s" Got: $logLine")
123+
false
124+
}
125+
}
126+
.foldLeft(true)(_ && _)
127+
else {
128+
System.err.println(s"Expected ${snapshotLines.length} lines, got ${logLines.length}")
129+
false
130+
}
131+
case None =>
132+
System.err.println(s"$snapshotPath not found")
133+
false
134+
}
135+
136+
if (updateSnapshots) {
137+
if (!matches) {
138+
System.err.println(s"Updating $snapshotPath")
139+
os.write.over(snapshotPath, logLines.mkString(System.lineSeparator()), createFolders = true)
140+
}
141+
} else
142+
assert(matches)
143+
}
144+
99145
val bspJsonrpcPool: ExecutorService = Executors.newCachedThreadPool(
100146
new ThreadFactory {
101147
val counter = new AtomicInteger
@@ -108,11 +154,16 @@ object BspServerTestUtil {
108154
)
109155

110156
trait MillBuildServer extends b.BuildServer with b.JvmBuildServer
111-
with b.JavaBuildServer with b.ScalaBuildServer
157+
with b.JavaBuildServer with b.ScalaBuildServer {
158+
@JsonRequest("millTest/loggingTest")
159+
def loggingTest(): CompletableFuture[Object]
160+
}
112161

113162
def withBspServer[T](
114163
workspacePath: os.Path,
115-
millTestSuiteEnv: Map[String, String]
164+
millTestSuiteEnv: Map[String, String],
165+
bspLog: Option[(Array[Byte], Int) => Unit] = None,
166+
client: b.BuildClient = DummyBuildClient
116167
)(f: (MillBuildServer, b.InitializeBuildResult) => T): T = {
117168

118169
val bspMetadataFile = workspacePath / Constants.bspDir / s"${Constants.serverName}.json"
@@ -131,16 +182,19 @@ object BspServerTestUtil {
131182
val proc = os.proc(bspCommand).spawn(
132183
cwd = workspacePath,
133184
stderr =
134-
if (outputOnErrorOnly)
185+
if (bspLog.isDefined || outputOnErrorOnly)
135186
os.ProcessOutput { (bytes, len) =>
136-
stderr.write(bytes, 0, len)
187+
if (outputOnErrorOnly)
188+
stderr.write(bytes, 0, len)
189+
else
190+
System.err.write(bytes, 0, len)
191+
for (f <- bspLog)
192+
f(bytes, len)
137193
}
138194
else os.Inherit,
139195
env = millTestSuiteEnv
140196
)
141197

142-
val client: b.BuildClient = DummyBuildClient
143-
144198
var success = false
145199
try {
146200
val launcher = new l.jsonrpc.Launcher.Builder[MillBuildServer]

0 commit comments

Comments
 (0)