@@ -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