@@ -2,8 +2,8 @@ package mill.daemon
2
2
3
3
import mill .api .SystemStreams
4
4
import mill .api .internal .internal
5
- import mill .define .PathRef
6
5
import mill .define .internal .Watchable
6
+ import mill .define .{PathRef , WorkspaceRoot }
7
7
import mill .internal .Colors
8
8
9
9
import java .io .InputStream
@@ -22,9 +22,15 @@ object Watching {
22
22
def apply (enterKeyPressed : Boolean , previousState : Option [T ]): Result [T ]
23
23
}
24
24
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
+ */
25
29
case class WatchArgs (
26
30
setIdle : Boolean => Unit ,
27
- colors : Colors
31
+ colors : Colors ,
32
+ useNotify : Boolean ,
33
+ serverDir : os.Path
28
34
)
29
35
30
36
/**
@@ -75,8 +81,7 @@ object Watching {
75
81
if (alreadyStale) {
76
82
enterKeyPressed = false
77
83
} else {
78
- enterKeyPressed =
79
- watchAndWait(streams, watchArgs.setIdle, streams.in, watchables, watchArgs.colors)
84
+ enterKeyPressed = watchAndWait(streams, streams.in, watchables, watchArgs)
80
85
}
81
86
}
82
87
throw new IllegalStateException (" unreachable" )
@@ -85,12 +90,11 @@ object Watching {
85
90
86
91
def watchAndWait (
87
92
streams : SystemStreams ,
88
- setIdle : Boolean => Unit ,
89
93
stdin : InputStream ,
90
94
watched : Seq [Watchable ],
91
- colors : Colors
95
+ watchArgs : WatchArgs
92
96
): Boolean = {
93
- setIdle(true )
97
+ watchArgs. setIdle(true )
94
98
val (watchedPollables, watchedPathsSeq) = watched.partitionMap {
95
99
case w : Watchable .Pollable => Left (w)
96
100
case p : Watchable .Path => Right (p)
@@ -101,42 +105,98 @@ object Watching {
101
105
val watchedValueStr =
102
106
if (watchedValueCount == 0 ) " " else s " and $watchedValueCount other values "
103
107
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) "
107
112
).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 )
138
118
enterKeyPressed
139
119
}
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()
140
200
}
141
201
142
202
/**
0 commit comments