@@ -2,8 +2,8 @@ package mill.daemon
22
33import mill .api .SystemStreams
44import mill .api .internal .internal
5- import mill .define .PathRef
65import mill .define .internal .Watchable
6+ import mill .define .{PathRef , WorkspaceRoot }
77import mill .internal .Colors
88
99import 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