Skip to content

Commit 3e7c22c

Browse files
committed
Forward porting from 0.12.x branch.
1 parent 4175938 commit 3e7c22c

File tree

4 files changed

+109
-42
lines changed

4 files changed

+109
-42
lines changed

build.mill

+1-1
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ object Deps {
159159
val junitInterface = mvn"com.github.sbt:junit-interface:0.13.3"
160160
val commonsIo = mvn"commons-io:commons-io:2.18.0"
161161
val log4j2Core = mvn"org.apache.logging.log4j:log4j-core:2.24.3"
162-
val osLibVersion = "0.11.5-M2-DIRTY1df4c00c"
162+
val osLibVersion = "0.11.5-M7"
163163
val osLib = mvn"com.lihaoyi::os-lib:${osLibVersion}"
164164
val osLibWatch = mvn"com.lihaoyi::os-lib-watch:${osLibVersion}"
165165
val pprint = mvn"com.lihaoyi::pprint:0.9.0"

runner/daemon/src/mill/daemon/MillCliConfig.scala

+5
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ case class MillCliConfig(
9797
doc = """Watch and re-run the given tasks when when their inputs change."""
9898
)
9999
watch: Flag = Flag(),
100+
@arg(
101+
name = "watch-via-fs-notify",
102+
doc = "Use filesystem based file watching instead of polling based one (defaults to true).",
103+
)
104+
watchViaFsNotify: Boolean = true,
100105
@arg(
101106
short = 's',
102107
doc =

runner/daemon/src/mill/daemon/MillMain.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,9 @@ object MillMain {
359359
ringBell = config.ringBell.value,
360360
watch = Option.when(config.watch.value)(Watching.WatchArgs(
361361
setIdle = setIdle,
362-
colors
362+
colors,
363+
useNotify = config.watchViaFsNotify,
364+
serverDir = serverDir
363365
)),
364366
streams = streams,
365367
evaluate = (enterKeyPressed: Boolean, prevState: Option[RunnerState]) => {

runner/daemon/src/mill/daemon/Watching.scala

+100-40
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package mill.daemon
22

33
import mill.api.SystemStreams
44
import mill.api.internal.internal
5-
import mill.define.PathRef
65
import mill.define.internal.Watchable
6+
import mill.define.{PathRef, WorkspaceRoot}
77
import mill.internal.Colors
88

99
import java.io.InputStream
@@ -22,9 +22,15 @@ object Watching {
2222
def apply(enterKeyPressed: Boolean, previousState: Option[T]): Result[T]
2323
}
2424

25+
/**
26+
* @param useNotify whether to use filesystem based watcher. If it is false uses polling.
27+
* @param serverDir the directory for storing logs of the mill server
28+
*/
2529
case class WatchArgs(
2630
setIdle: Boolean => Unit,
27-
colors: Colors
31+
colors: Colors,
32+
useNotify: Boolean,
33+
serverDir: os.Path
2834
)
2935

3036
/**
@@ -75,8 +81,7 @@ object Watching {
7581
if (alreadyStale) {
7682
enterKeyPressed = false
7783
} else {
78-
enterKeyPressed =
79-
watchAndWait(streams, watchArgs.setIdle, streams.in, watchables, watchArgs.colors)
84+
enterKeyPressed = watchAndWait(streams, streams.in, watchables, watchArgs)
8085
}
8186
}
8287
throw new IllegalStateException("unreachable")
@@ -85,12 +90,11 @@ object Watching {
8590

8691
def watchAndWait(
8792
streams: SystemStreams,
88-
setIdle: Boolean => Unit,
8993
stdin: InputStream,
9094
watched: Seq[Watchable],
91-
colors: Colors
95+
watchArgs: WatchArgs
9296
): Boolean = {
93-
setIdle(true)
97+
watchArgs.setIdle(true)
9498
val (watchedPollables, watchedPathsSeq) = watched.partitionMap {
9599
case w: Watchable.Pollable => Left(w)
96100
case p: Watchable.Path => Right(p)
@@ -101,42 +105,98 @@ object Watching {
101105
val watchedValueStr =
102106
if (watchedValueCount == 0) "" else s" and $watchedValueCount other values"
103107

104-
streams.err.println(
105-
colors.info(
106-
s"Watching for changes to ${watchedPathsSeq.size} paths$watchedValueStr... (Enter to re-run, Ctrl-C to exit)"
108+
streams.err.println {
109+
val viaFsNotify = if (watchArgs.useNotify) " (via fsnotify)" else ""
110+
watchArgs.colors.info(
111+
s"Watching for changes to ${watchedPathsSeq.size} paths$viaFsNotify$watchedValueStr... (Enter to re-run, Ctrl-C to exit)"
107112
).toString
108-
)
109-
110-
@volatile var pathChangesDetected = false
111-
112-
// oslib watch only works with folders, so we have to watch the parent folders instead
113-
val osLibWatchPaths = watchedPathsSet.iterator.map(p => p / "..").toSet
114-
// mill.constants.DebugLog(
115-
// colors.info(s"[watch:watched-paths] ${osLibWatchPaths.mkString("\n")}").toString
116-
// )
117-
118-
Using.resource(os.watch.watch(
119-
osLibWatchPaths.toSeq,
120-
onEvent = changedPaths => {
121-
// Make sure that the changed paths are actually the ones in our watch list and not some adjacent files in the
122-
// same folder
123-
val hasWatchedPath = changedPaths.exists(p => watchedPathsSet.contains(p))
124-
// mill.constants.DebugLog(colors.info(
125-
// s"[watch:changed-paths] (hasWatchedPath=$hasWatchedPath) ${changedPaths.mkString("\n")}"
126-
// ).toString)
127-
if (hasWatchedPath) {
128-
pathChangesDetected = true
129-
}
130-
},
131-
// logger = (eventType, data) => {
132-
// mill.constants.DebugLog(colors.info(s"[watch] $eventType: ${pprint.apply(data)}").toString)
133-
// }
134-
)) { _ =>
135-
val enterKeyPressed =
136-
statWatchWait(watchedPollables, stdin, notifiablesChanged = () => pathChangesDetected)
137-
setIdle(false)
113+
}
114+
115+
def doWatch(notifiablesChanged: () => Boolean) = {
116+
val enterKeyPressed = statWatchWait(watchedPollables, stdin, notifiablesChanged)
117+
watchArgs.setIdle(false)
138118
enterKeyPressed
139119
}
120+
121+
def doWatchPolling() =
122+
doWatch(notifiablesChanged = () => watchedPathsSeq.exists(p => !validateAnyWatchable(p)))
123+
124+
def doWatchFsNotify() = {
125+
Using.resource(os.write.outputStream(watchArgs.serverDir / "fsNotifyWatchLog")) { watchLog =>
126+
def writeToWatchLog(s: String): Unit = {
127+
watchLog.write(s.getBytes(java.nio.charset.StandardCharsets.UTF_8))
128+
watchLog.write('\n')
129+
}
130+
131+
@volatile var pathChangesDetected = false
132+
133+
// oslib watch only works with folders, so we have to watch the parent folders instead
134+
135+
writeToWatchLog(
136+
s"[watched-paths:unfiltered] ${watchedPathsSet.toSeq.sorted.mkString("\n")}"
137+
)
138+
139+
val workspaceRoot = WorkspaceRoot.workspaceRoot
140+
141+
/** Paths that are descendants of [[workspaceRoot]]. */
142+
val pathsUnderWorkspaceRoot = watchedPathsSet.filter { path =>
143+
val isUnderWorkspaceRoot = path.startsWith(workspaceRoot)
144+
if (!isUnderWorkspaceRoot) {
145+
streams.err.println(watchArgs.colors.error(
146+
s"Watched path $path is outside workspace root $workspaceRoot, this is unsupported."
147+
).toString())
148+
}
149+
150+
isUnderWorkspaceRoot
151+
}
152+
153+
// If I have 'root/a/b/c'
154+
//
155+
// Then I want to watch:
156+
// root/a/b/c
157+
// root/a/b
158+
// root/a
159+
// root
160+
val filterPaths = pathsUnderWorkspaceRoot.flatMap { path =>
161+
path.relativeTo(workspaceRoot).segments.inits.map(segments => workspaceRoot / segments)
162+
}
163+
writeToWatchLog(s"[watched-paths:filtered] ${filterPaths.toSeq.sorted.mkString("\n")}")
164+
165+
Using.resource(os.watch.watch(
166+
// Just watch the root folder
167+
Seq(workspaceRoot),
168+
filter = path => {
169+
val shouldBeWatched =
170+
filterPaths.contains(path) || watchedPathsSet.exists(watchedPath =>
171+
path.startsWith(watchedPath)
172+
)
173+
writeToWatchLog(s"[filter] (shouldBeWatched=$shouldBeWatched) $path")
174+
shouldBeWatched
175+
},
176+
onEvent = changedPaths => {
177+
// Make sure that the changed paths are actually the ones in our watch list and not some adjacent files in the
178+
// same folder
179+
val hasWatchedPath =
180+
changedPaths.exists(p =>
181+
watchedPathsSet.exists(watchedPath => p.startsWith(watchedPath))
182+
)
183+
writeToWatchLog(
184+
s"[changed-paths] (hasWatchedPath=$hasWatchedPath) ${changedPaths.mkString("\n")}"
185+
)
186+
if (hasWatchedPath) {
187+
pathChangesDetected = true
188+
}
189+
},
190+
logger = (eventType, data) =>
191+
writeToWatchLog(s"[watch:event] $eventType: ${pprint.apply(data).plainText}")
192+
)) { _ =>
193+
doWatch(notifiablesChanged = () => pathChangesDetected)
194+
}
195+
}
196+
}
197+
198+
if (watchArgs.useNotify) doWatchFsNotify()
199+
else doWatchPolling()
140200
}
141201

142202
/**

0 commit comments

Comments
 (0)