Skip to content

Commit d9593de

Browse files
committed
Initial version.
1 parent 1be73fd commit d9593de

File tree

5 files changed

+139
-45
lines changed

5 files changed

+139
-45
lines changed

build.mill

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ object Deps {
150150
val junitInterface = ivy"com.github.sbt:junit-interface:0.13.3"
151151
val commonsIo = ivy"commons-io:commons-io:2.18.0"
152152
val log4j2Core = ivy"org.apache.logging.log4j:log4j-core:2.24.3"
153-
val osLib = ivy"com.lihaoyi::os-lib:0.11.4-M6"
153+
val osLibVersion = "0.11.5-M2-DIRTYc8bf6115"
154+
val osLib = ivy"com.lihaoyi::os-lib:${osLibVersion}"
155+
val osLibWatch = ivy"com.lihaoyi::os-lib-watch:${osLibVersion}"
154156
val pprint = ivy"com.lihaoyi::pprint:0.9.0"
155157
val mainargs = ivy"com.lihaoyi::mainargs:0.7.6"
156158
val millModuledefsVersion = "0.11.2"

main/util/src/mill/util/Watchable.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ import mill.api.internal
1010
*/
1111
@internal
1212
private[mill] trait Watchable {
13+
/** @return the hashcode of a watched value. */
1314
def poll(): Long
15+
16+
/** The initial hashcode of a watched value. */
1417
def signature: Long
18+
1519
def validate(): Boolean = poll() == signature
20+
1621
def pretty: String
1722
}
1823
@internal

runner/package.mill

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ object `package` extends RootModule with build.MillPublishScalaModule {
1313
build.Deps.windowsAnsi,
1414
build.Deps.coursier,
1515
build.Deps.coursierJvm,
16-
build.Deps.logback
16+
build.Deps.logback,
17+
build.Deps.osLibWatch
1718
)
1819
def buildInfoObjectName = "Versions"
1920
def buildInfoMembers = Seq(

runner/src/mill/runner/MillMain.scala

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,8 @@ object MillMain {
231231
}
232232
val (isSuccess, evalStateOpt) = Watching.watchLoop(
233233
ringBell = config.ringBell.value,
234-
watch = config.watch.value,
234+
watch = Option.when(config.watch.value)(Watching.WatchArgs(setIdle, colors)),
235235
streams = streams,
236-
setIdle = setIdle,
237236
evaluate = (enterKeyPressed: Boolean, prevState: Option[RunnerState]) => {
238237
adjustJvmProperties(userSpecifiedProperties, initialSystemProperties)
239238

@@ -285,8 +284,7 @@ object MillMain {
285284
}
286285
}
287286
}
288-
},
289-
colors = colors
287+
}
290288
)
291289
bspContext.foreach { ctx =>
292290
repeatForBsp =

runner/src/mill/runner/Watching.scala

Lines changed: 127 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package mill.runner
33
import mill.api.internal
44
import mill.util.{Colors, Watchable}
55
import mill.api.SystemStreams
6+
import mill.main.client.DebugLog
67

78
import java.io.InputStream
89
import scala.annotation.tailrec
10+
import scala.util.Using
911

1012
/**
1113
* Logic around the "watch and wait" functionality in Mill: re-run on change,
@@ -15,40 +17,65 @@ import scala.annotation.tailrec
1517
object Watching {
1618
case class Result[T](watched: Seq[Watchable], error: Option[String], result: T)
1719

20+
trait Evaluate[T] {
21+
def apply(enterKeyPressed: Boolean, previousState: Option[T]): Result[T]
22+
}
23+
24+
case class WatchArgs(
25+
setIdle: Boolean => Unit,
26+
colors: Colors
27+
)
28+
1829
def watchLoop[T](
1930
ringBell: Boolean,
20-
watch: Boolean,
31+
watch: Option[WatchArgs],
2132
streams: SystemStreams,
22-
setIdle: Boolean => Unit,
23-
evaluate: (Boolean, Option[T]) => Result[T],
24-
colors: Colors
33+
evaluate: Evaluate[T],
2534
): (Boolean, T) = {
26-
var prevState: Option[T] = None
27-
var enterKeyPressed = false
28-
while (true) {
29-
val Result(watchables, errorOpt, result) = evaluate(enterKeyPressed, prevState)
30-
prevState = Some(result)
35+
def handleError(errorOpt: Option[String]): Unit = {
3136
errorOpt.foreach(streams.err.println)
32-
if (ringBell) {
33-
if (errorOpt.isEmpty) println("\u0007")
34-
else {
35-
println("\u0007")
36-
Thread.sleep(250)
37-
println("\u0007")
38-
}
39-
}
37+
doRingBell(hasError = errorOpt.isDefined)
38+
}
4039

41-
if (!watch) {
42-
return (errorOpt.isEmpty, result)
43-
}
40+
def doRingBell(hasError: Boolean): Unit = {
41+
if (!ringBell) return
4442

45-
val alreadyStale = watchables.exists(!_.validate())
46-
enterKeyPressed = false
47-
if (!alreadyStale) {
48-
enterKeyPressed = Watching.watchAndWait(streams, setIdle, streams.in, watchables, colors)
43+
println("\u0007")
44+
if (hasError) {
45+
// If we have an error ring the bell again
46+
Thread.sleep(250)
47+
println("\u0007")
4948
}
5049
}
51-
???
50+
51+
watch match {
52+
case None =>
53+
val Result(watchables, errorOpt, result) =
54+
evaluate(enterKeyPressed = false, previousState = None)
55+
handleError(errorOpt)
56+
(errorOpt.isEmpty, result)
57+
58+
case Some(watchArgs) =>
59+
var prevState: Option[T] = None
60+
var enterKeyPressed = false
61+
62+
// Exits when the thread gets interruped.
63+
while (true) {
64+
val Result(watchables, errorOpt, result) = evaluate(enterKeyPressed, prevState)
65+
prevState = Some(result)
66+
handleError(errorOpt)
67+
68+
// Do not enter watch if already stale, re-evaluate instantly.
69+
val alreadyStale = watchables.exists(w => !w.validate())
70+
if (alreadyStale) {
71+
enterKeyPressed = false
72+
} else {
73+
enterKeyPressed =
74+
watchAndWait(streams, watchArgs.setIdle, streams.in, watchables, watchArgs.colors)
75+
}
76+
}
77+
throw new IllegalStateException("unreachable")
78+
}
5279
}
5380

5481
def watchAndWait(
@@ -59,28 +86,88 @@ object Watching {
5986
colors: Colors
6087
): Boolean = {
6188
setIdle(true)
62-
val watchedPaths = watched.collect { case p: Watchable.Path => p.p.path }
63-
val watchedValues = watched.size - watchedPaths.size
89+
val (watchedPollables, watchedPathsSeq) = watched.partitionMap {
90+
case w: Watchable.Value => Left(w)
91+
case p: Watchable.Path => Right(p)
92+
}
93+
val watchedPathsSet = watchedPathsSeq.iterator.map(p => p.p.path).toSet
94+
val watchedValueCount = watched.size - watchedPathsSeq.size
6495

65-
val watchedValueStr = if (watchedValues == 0) "" else s" and $watchedValues other values"
96+
val watchedValueStr =
97+
if (watchedValueCount == 0) "" else s" and $watchedValueCount other values"
6698

6799
streams.err.println(
68100
colors.info(
69-
s"Watching for changes to ${watchedPaths.size} paths$watchedValueStr... (Enter to re-run, Ctrl-C to exit)"
101+
s"Watching for changes to ${watchedPathsSeq.size} paths$watchedValueStr... (Enter to re-run, Ctrl-C to exit)"
70102
).toString
71103
)
72104

73-
val enterKeyPressed = statWatchWait(watched, stdin)
74-
setIdle(false)
75-
enterKeyPressed
105+
@volatile var pathChangesDetected = false
106+
107+
// oslib watch only works with folders, so we have to watch the parent folders instead
108+
109+
mill.main.client.DebugLog.println(
110+
colors.info(s"[watch:watched-paths:unfiltered] ${watchedPathsSet.toSeq.sorted.mkString("\n")}").toString
111+
)
112+
113+
val ignoredFolders = Seq(
114+
mill.api.WorkspaceRoot.workspaceRoot / "out",
115+
mill.api.WorkspaceRoot.workspaceRoot / ".bloop",
116+
mill.api.WorkspaceRoot.workspaceRoot / ".metals",
117+
mill.api.WorkspaceRoot.workspaceRoot / ".idea",
118+
mill.api.WorkspaceRoot.workspaceRoot / ".git",
119+
mill.api.WorkspaceRoot.workspaceRoot / ".bsp",
120+
)
121+
mill.main.client.DebugLog.println(
122+
colors.info(s"[watch:ignored-paths] ${ignoredFolders.toSeq.sorted.mkString("\n")}").toString
123+
)
124+
125+
val osLibWatchPaths = watchedPathsSet.iterator.map(p => p / "..").toSet
126+
mill.main.client.DebugLog.println(
127+
colors.info(s"[watch:watched-paths] ${osLibWatchPaths.toSeq.sorted.mkString("\n")}").toString
128+
)
129+
130+
Using.resource(os.watch.watch(
131+
osLibWatchPaths.toSeq,
132+
// filter = path => {
133+
// val shouldBeIgnored = ignoredFolders.exists(ignored => path.startsWith(ignored))
134+
// mill.main.client.DebugLog.println(
135+
// colors.info(s"[watch:filter] $path (ignored=$shouldBeIgnored), ignoredFolders=${ignoredFolders.mkString("[\n ", "\n ", "\n]")}").toString
136+
// )
137+
// !shouldBeIgnored
138+
// },
139+
onEvent = changedPaths => {
140+
// Make sure that the changed paths are actually the ones in our watch list and not some adjacent files in the
141+
// same folder
142+
val hasWatchedPath = changedPaths.exists(p => watchedPathsSet.exists(watchedPath => p.startsWith(watchedPath)))
143+
mill.main.client.DebugLog.println(colors.info(
144+
s"[watch:changed-paths] (hasWatchedPath=$hasWatchedPath) ${changedPaths.mkString("\n")}"
145+
).toString)
146+
if (hasWatchedPath) {
147+
pathChangesDetected = true
148+
}
149+
},
150+
logger = (eventType, data) => {
151+
mill.main.client.DebugLog.println(colors.info(s"[watch] $eventType: ${pprint.apply(data)}").toString)
152+
}
153+
)) { _ =>
154+
val enterKeyPressed =
155+
statWatchWait(watchedPollables, stdin, notifiablesChanged = () => pathChangesDetected)
156+
setIdle(false)
157+
enterKeyPressed
158+
}
76159
}
77160

78-
// Returns `true` if enter key is pressed to re-run tasks explicitly
79-
def statWatchWait(watched: Seq[Watchable], stdin: InputStream): Boolean = {
161+
/**
162+
* @param notifiablesChanged returns true if any of the notifiables have changed
163+
*
164+
* @return `true` if enter key is pressed to re-run tasks explicitly, false if changes in watched files occured.
165+
*/
166+
def statWatchWait(watched: Seq[Watchable], stdin: InputStream, notifiablesChanged: () => Boolean): Boolean = {
80167
val buffer = new Array[Byte](4 * 1024)
81168

82169
@tailrec def statWatchWait0(): Boolean = {
83-
if (watched.forall(_.validate())) {
170+
if (!notifiablesChanged() && watched.forall(_.validate())) {
84171
if (lookForEnterKey()) {
85172
true
86173
} else {
@@ -94,17 +181,18 @@ object Watching {
94181
if (stdin.available() == 0) false
95182
else stdin.read(buffer) match {
96183
case 0 | -1 => false
97-
case n =>
184+
case bytesRead =>
98185
buffer.indexOf('\n') match {
99186
case -1 => lookForEnterKey()
100-
case i =>
101-
if (i >= n) lookForEnterKey()
187+
case index =>
188+
// If we found the newline further than the bytes read, that means it's not from this read and thus we
189+
// should try reading again.
190+
if (index >= bytesRead) lookForEnterKey()
102191
else true
103192
}
104193
}
105194
}
106195

107196
statWatchWait0()
108197
}
109-
110198
}

0 commit comments

Comments
 (0)