Skip to content

Commit 1ac4c57

Browse files
committed
Allow toggling --watch mode with --watch-via-fs-notify=true
1 parent 11d164a commit 1ac4c57

File tree

4 files changed

+98
-68
lines changed

4 files changed

+98
-68
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ private[mill] trait Watchable {
1616
/** The initial hashcode of a watched value. */
1717
def signature: Long
1818

19+
/** @return true if the watched value has not changed */
1920
def validate(): Boolean = poll() == signature
2021

2122
def pretty: String

runner/src/mill/runner/MillCliConfig.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ case class MillCliConfig(
9595
doc = """Watch and re-run the given tasks when when their inputs change."""
9696
)
9797
watch: Flag = Flag(),
98+
@arg(
99+
name = "watch-via-fs-notify",
100+
doc = "Use filesystem based file watching instead of polling based one (experimental, defaults to false).",
101+
)
102+
watchViaFsNotify: Boolean = false,
98103
@arg(
99104
short = 's',
100105
doc =

runner/src/mill/runner/MillMain.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,9 @@ object MillMain {
231231
}
232232
val (isSuccess, evalStateOpt) = Watching.watchLoop(
233233
ringBell = config.ringBell.value,
234-
watch = Option.when(config.watch.value)(Watching.WatchArgs(setIdle, colors)),
234+
watch = Option.when(config.watch.value)(Watching.WatchArgs(
235+
setIdle, colors, useNotify = config.watchViaFsNotify
236+
)),
235237
streams = streams,
236238
evaluate = (enterKeyPressed: Boolean, prevState: Option[RunnerState]) => {
237239
adjustJvmProperties(userSpecifiedProperties, initialSystemProperties)

runner/src/mill/runner/Watching.scala

Lines changed: 89 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@ 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+
*/
2528
case class WatchArgs(
2629
setIdle: Boolean => Unit,
27-
colors: Colors
30+
colors: Colors,
31+
useNotify: Boolean
2832
)
2933

3034
def watchLoop[T](
@@ -71,8 +75,14 @@ object Watching {
7175
if (alreadyStale) {
7276
enterKeyPressed = false
7377
} else {
74-
enterKeyPressed =
75-
watchAndWait(streams, watchArgs.setIdle, streams.in, watchables, watchArgs.colors)
78+
enterKeyPressed = watchAndWait(
79+
streams,
80+
watchArgs.setIdle,
81+
streams.in,
82+
watchables,
83+
watchArgs.colors,
84+
useNotify = watchArgs.useNotify
85+
)
7686
}
7787
}
7888
throw new IllegalStateException("unreachable")
@@ -84,7 +94,8 @@ object Watching {
8494
setIdle: Boolean => Unit,
8595
stdin: InputStream,
8696
watched: Seq[Watchable],
87-
colors: Colors
97+
colors: Colors,
98+
useNotify: Boolean
8899
): Boolean = {
89100
setIdle(true)
90101
val (watchedPollables, watchedPathsSeq) = watched.partitionMap {
@@ -97,75 +108,86 @@ object Watching {
97108
val watchedValueStr =
98109
if (watchedValueCount == 0) "" else s" and $watchedValueCount other values"
99110

100-
streams.err.println(
111+
streams.err.println {
112+
val viaFsNotify = if (useNotify) " (via fsnotify)" else ""
101113
colors.info(
102-
s"Watching for changes to ${watchedPathsSeq.size} paths$watchedValueStr... (Enter to re-run, Ctrl-C to exit)"
114+
s"Watching for changes to ${watchedPathsSeq.size} paths$viaFsNotify$watchedValueStr... (Enter to re-run, Ctrl-C to exit)"
103115
).toString
104-
)
105-
106-
@volatile var pathChangesDetected = false
107-
108-
// oslib watch only works with folders, so we have to watch the parent folders instead
116+
}
109117

110-
if (enableDebugLog) DebugLog.println(
111-
colors.info(
112-
s"[watch:watched-paths:unfiltered] ${watchedPathsSet.toSeq.sorted.mkString("\n")}"
113-
).toString
114-
)
115-
116-
/** A hardcoded list of folders to ignore that we know have no impact on the build. */
117-
val ignoredFolders = Seq(
118-
mill.api.WorkspaceRoot.workspaceRoot / "out",
119-
mill.api.WorkspaceRoot.workspaceRoot / ".bloop",
120-
mill.api.WorkspaceRoot.workspaceRoot / ".metals",
121-
mill.api.WorkspaceRoot.workspaceRoot / ".idea",
122-
mill.api.WorkspaceRoot.workspaceRoot / ".git",
123-
mill.api.WorkspaceRoot.workspaceRoot / ".bsp"
124-
)
125-
if (enableDebugLog) DebugLog.println(
126-
colors.info(s"[watch:ignored-paths] ${ignoredFolders.toSeq.sorted.mkString("\n")}").toString
127-
)
128-
129-
val osLibWatchPaths = watchedPathsSet.iterator.map(p => p / "..").toSet
130-
if (enableDebugLog) DebugLog.println(
131-
colors.info(s"[watch:watched-paths] ${osLibWatchPaths.toSeq.sorted.mkString("\n")}").toString
132-
)
133-
134-
Using.resource(os.watch.watch(
135-
osLibWatchPaths.toSeq,
136-
filter = path => {
137-
val shouldBeIgnored = ignoredFolders.exists(ignored => path.startsWith(ignored))
138-
if (enableDebugLog) {
139-
val ignoredFoldersStr = ignoredFolders.mkString("[\n ", "\n ", "\n]")
140-
DebugLog.println(
141-
colors.info(s"[watch:filter] $path (ignored=$shouldBeIgnored), ignoredFolders=$ignoredFoldersStr").toString
142-
)
143-
}
144-
!shouldBeIgnored
145-
},
146-
onEvent = changedPaths => {
147-
// Make sure that the changed paths are actually the ones in our watch list and not some adjacent files in the
148-
// same folder
149-
val hasWatchedPath =
150-
changedPaths.exists(p => watchedPathsSet.exists(watchedPath => p.startsWith(watchedPath)))
151-
if (enableDebugLog) DebugLog.println(colors.info(
152-
s"[watch:changed-paths] (hasWatchedPath=$hasWatchedPath) ${changedPaths.mkString("\n")}"
153-
).toString)
154-
if (hasWatchedPath) {
155-
pathChangesDetected = true
156-
}
157-
},
158-
logger =
159-
if (enableDebugLog) (eventType, data) => {
160-
DebugLog.println(colors.info(s"[watch] $eventType: ${pprint.apply(data)}").toString)
161-
}
162-
else (_, _) => {}
163-
)) { _ =>
164-
val enterKeyPressed =
165-
statWatchWait(watchedPollables, stdin, notifiablesChanged = () => pathChangesDetected)
118+
def doWatch(notifiablesChanged: () => Boolean) = {
119+
val enterKeyPressed = statWatchWait(watchedPollables, stdin, notifiablesChanged)
166120
setIdle(false)
167121
enterKeyPressed
168122
}
123+
124+
if (useNotify) {
125+
@volatile var pathChangesDetected = false
126+
127+
// oslib watch only works with folders, so we have to watch the parent folders instead
128+
129+
if (enableDebugLog) DebugLog.println(
130+
colors.info(
131+
s"[watch:watched-paths:unfiltered] ${watchedPathsSet.toSeq.sorted.mkString("\n")}"
132+
).toString
133+
)
134+
135+
/** A hardcoded list of folders to ignore that we know have no impact on the build. */
136+
val ignoredFolders = Seq(
137+
mill.api.WorkspaceRoot.workspaceRoot / "out",
138+
mill.api.WorkspaceRoot.workspaceRoot / ".bloop",
139+
mill.api.WorkspaceRoot.workspaceRoot / ".metals",
140+
mill.api.WorkspaceRoot.workspaceRoot / ".idea",
141+
mill.api.WorkspaceRoot.workspaceRoot / ".git",
142+
mill.api.WorkspaceRoot.workspaceRoot / ".bsp"
143+
)
144+
if (enableDebugLog) DebugLog.println(
145+
colors.info(s"[watch:ignored-paths] ${ignoredFolders.toSeq.sorted.mkString("\n")}").toString
146+
)
147+
148+
val osLibWatchPaths = watchedPathsSet.iterator.map(p => p / "..").toSet
149+
if (enableDebugLog) DebugLog.println(
150+
colors.info(s"[watch:watched-paths] ${osLibWatchPaths.toSeq.sorted.mkString("\n")}").toString
151+
)
152+
153+
Using.resource(os.watch.watch(
154+
osLibWatchPaths.toSeq,
155+
filter = path => {
156+
val shouldBeIgnored = ignoredFolders.exists(ignored => path.startsWith(ignored))
157+
if (enableDebugLog) {
158+
val ignoredFoldersStr = ignoredFolders.mkString("[\n ", "\n ", "\n]")
159+
DebugLog.println(
160+
colors.info(
161+
s"[watch:filter] $path (ignored=$shouldBeIgnored), ignoredFolders=$ignoredFoldersStr"
162+
).toString
163+
)
164+
}
165+
!shouldBeIgnored
166+
},
167+
onEvent = changedPaths => {
168+
// Make sure that the changed paths are actually the ones in our watch list and not some adjacent files in the
169+
// same folder
170+
val hasWatchedPath =
171+
changedPaths.exists(p => watchedPathsSet.exists(watchedPath => p.startsWith(watchedPath)))
172+
if (enableDebugLog) DebugLog.println(colors.info(
173+
s"[watch:changed-paths] (hasWatchedPath=$hasWatchedPath) ${changedPaths.mkString("\n")}"
174+
).toString)
175+
if (hasWatchedPath) {
176+
pathChangesDetected = true
177+
}
178+
},
179+
logger =
180+
if (enableDebugLog) (eventType, data) => {
181+
DebugLog.println(colors.info(s"[watch] $eventType: ${pprint.apply(data)}").toString)
182+
}
183+
else (_, _) => {}
184+
)) { _ =>
185+
doWatch(notifiablesChanged = () => pathChangesDetected)
186+
}
187+
}
188+
else {
189+
doWatch(notifiablesChanged = () => watchedPathsSeq.exists(p => !p.validate()))
190+
}
169191
}
170192

171193
/**

0 commit comments

Comments
 (0)