@@ -3,7 +3,8 @@ package mill.daemon
3
3
import mill .api .SystemStreams
4
4
import mill .api .internal .internal
5
5
import mill .define .internal .Watchable
6
- import mill .define .{PathRef , WorkspaceRoot }
6
+ import mill .define .PathRef
7
+ import mill .define .WorkspaceRoot .workspaceRoot
7
8
import mill .internal .Colors
8
9
9
10
import java .io .InputStream
@@ -34,6 +35,12 @@ object Watching {
34
35
serverDir : os.Path
35
36
)
36
37
38
+ private case class WatchViaNotifyArgs (
39
+ notifiablesChanged : () => Boolean
40
+ )
41
+
42
+ class MutableCell [A ](var value : A )
43
+
37
44
/**
38
45
* @param ringBell whether to emit bells
39
46
* @param watch if [[None ]] just runs once and returns
@@ -71,40 +78,141 @@ object Watching {
71
78
var prevState : Option [T ] = None
72
79
var enterKeyPressed = false
73
80
74
- // Exits when the thread gets interruped.
75
- while (true ) {
81
+ def evaluateOnce () = {
76
82
val Result (watchables, errorOpt, result) = evaluate(enterKeyPressed, prevState)
77
83
prevState = Some (result)
78
84
handleError(errorOpt)
85
+ WatchedFiles (watchables, streams, watchArgs.colors)
86
+ }
79
87
80
- // Do not enter watch if already stale, re-evaluate instantly.
81
- val alreadyStale = watchables.exists(w => ! validateAnyWatchable(w))
82
- if (alreadyStale) {
83
- enterKeyPressed = false
84
- } else {
85
- enterKeyPressed = watchAndWait(streams, streams.in, watchables, watchArgs)
88
+ def loop (watchViaNotify : Option [WatchViaNotifyArgs ]): Nothing = {
89
+ // Exits when the thread gets interruped.
90
+ while (true ) {
91
+ val watchables = evaluateOnce()
92
+
93
+ // Do not enter watch if already stale, re-evaluate instantly.
94
+ val alreadyStale = watchables.watched.exists(w => ! validateAnyWatchable(w))
95
+ if (alreadyStale) {
96
+ enterKeyPressed = false
97
+ } else {
98
+ enterKeyPressed = watchAndWait(streams, streams.in, watchables, watchArgs, watchViaNotify)
99
+ }
86
100
}
101
+ throw new IllegalStateException (" unreachable" )
87
102
}
88
- throw new IllegalStateException (" unreachable" )
103
+
104
+ if (watchArgs.useNotify) {
105
+ Using .resource(os.write.outputStream(watchArgs.serverDir / " fsNotifyWatchLog" )) { watchLog =>
106
+ def writeToWatchLog (s : String ): Unit = {
107
+ try {
108
+ watchLog.write(s.getBytes(java.nio.charset.StandardCharsets .UTF_8 ))
109
+ watchLog.write('\n ' )
110
+ } catch {
111
+ case _ : ClosedChannelException => /* do nothing, the file is already closed */
112
+ }
113
+ }
114
+
115
+ val watchedFiles = evaluateOnce()
116
+ writeToWatchLog(s " [watched-paths:unfiltered] ${watchedFiles.watchedPathsSet.toSeq.sorted.mkString(" \n " )}" )
117
+ writeToWatchLog(s " [watched-paths:filtered] ${watchedFiles.filterPaths.toSeq.sorted.mkString(" \n " )}" )
118
+
119
+ // Start the watch before entering the evaluation loop to make sure no events fall through.
120
+ @ volatile var pathChangesDetected = false
121
+ Using .resource(os.watch.watch(
122
+ // Just watch the root folder
123
+ Seq (workspaceRoot),
124
+ filter = path => {
125
+ val shouldBeWatched =
126
+ watchedFiles.filterPaths.contains(path) || watchedFiles.watchedPathsSet.exists(watchedPath =>
127
+ path.startsWith(watchedPath)
128
+ )
129
+ writeToWatchLog(s " [filter] (shouldBeWatched= $shouldBeWatched) $path" )
130
+ shouldBeWatched
131
+ },
132
+ onEvent = changedPaths => {
133
+ // Make sure that the changed paths are actually the ones in our watch list and not some adjacent files in the
134
+ // same folder
135
+ val hasWatchedPath =
136
+ changedPaths.exists(p =>
137
+ watchedFiles.watchedPathsSet.exists(watchedPath => p.startsWith(watchedPath))
138
+ )
139
+ writeToWatchLog(
140
+ s " [changed-paths] (hasWatchedPath= $hasWatchedPath) ${changedPaths.mkString(" \n " )}"
141
+ )
142
+ if (hasWatchedPath) {
143
+ pathChangesDetected = true
144
+ }
145
+ },
146
+ logger = (eventType, data) =>
147
+ writeToWatchLog(s " [watch:event] $eventType: ${pprint.apply(data).plainText}" )
148
+ )) { _ =>
149
+ loop(Some (WatchViaNotifyArgs (notifiablesChanged = () => pathChangesDetected)))
150
+ }
151
+ }
152
+ } else loop(watchViaNotify = None )
89
153
}
90
154
}
91
155
92
- def watchAndWait (
156
+ private case class WatchedFiles (
157
+ watched : Seq [Watchable ],
158
+ watchedPollables : Seq [Watchable .Pollable ],
159
+ watchedPathsSeq : Seq [Watchable .Path ],
160
+ watchedPathsSet : Set [os.Path ],
161
+ filterPaths : Set [os.Path ]
162
+ ) {
163
+ def watchedValueCount : Int = watched.size - watchedPathsSeq.size
164
+
165
+ def watchedValueStr : String =
166
+ if (watchedValueCount == 0 ) " " else s " and $watchedValueCount other values "
167
+ }
168
+ private object WatchedFiles {
169
+ def apply (watched : Seq [Watchable ], streams : SystemStreams , colors : Colors ): WatchedFiles = {
170
+ val (watchedPollables, watchedPathsSeq) = watched.partitionMap {
171
+ case w : Watchable .Pollable => Left (w)
172
+ case p : Watchable .Path => Right (p)
173
+ }
174
+
175
+ val watchedPathsSet : Set [os.Path ] = watchedPathsSeq.iterator.map(p => os.Path (p.p)).toSet
176
+
177
+ /** Paths that are descendants of [[workspaceRoot ]]. */
178
+ val pathsUnderWorkspaceRoot = {
179
+ watchedPathsSet.filter { path =>
180
+ val isUnderWorkspaceRoot = path.startsWith(workspaceRoot)
181
+ if (! isUnderWorkspaceRoot) streams.err.println(colors.error(
182
+ s " Watched path $path is outside workspace root $workspaceRoot, this is unsupported. "
183
+ ).toString())
184
+
185
+ isUnderWorkspaceRoot
186
+ }
187
+ }
188
+
189
+ // If I have 'root/a/b/c'
190
+ //
191
+ // Then I want to watch:
192
+ // root/a/b/c
193
+ // root/a/b
194
+ // root/a
195
+ // root
196
+ val filterPaths = pathsUnderWorkspaceRoot.flatMap { path =>
197
+ path.relativeTo(workspaceRoot).segments.inits.map(segments => workspaceRoot / segments)
198
+ }
199
+
200
+ apply(
201
+ watched = watched, watchedPollables = watchedPollables, watchedPathsSeq = watchedPathsSeq,
202
+ watchedPathsSet = watchedPathsSet, filterPaths = filterPaths
203
+ )
204
+ }
205
+ }
206
+
207
+ private def watchAndWait (
93
208
streams : SystemStreams ,
94
209
stdin : InputStream ,
95
- watched : Seq [Watchable ],
96
- watchArgs : WatchArgs
210
+ watched : WatchedFiles ,
211
+ watchArgs : WatchArgs ,
212
+ watchViaNotifyArgs : Option [WatchViaNotifyArgs ]
97
213
): Boolean = {
98
214
watchArgs.setIdle(true )
99
- val (watchedPollables, watchedPathsSeq) = watched.partitionMap {
100
- case w : Watchable .Pollable => Left (w)
101
- case p : Watchable .Path => Right (p)
102
- }
103
- val watchedPathsSet = watchedPathsSeq.iterator.map(p => os.Path (p.p)).toSet
104
- val watchedValueCount = watched.size - watchedPathsSeq.size
105
-
106
- val watchedValueStr =
107
- if (watchedValueCount == 0 ) " " else s " and $watchedValueCount other values "
215
+ import watched .{watchedValueStr , watchedPollables , watchedPathsSeq }
108
216
109
217
streams.err.println {
110
218
val viaFsNotify = if (watchArgs.useNotify) " (via fsnotify)" else " "
@@ -119,89 +227,10 @@ object Watching {
119
227
enterKeyPressed
120
228
}
121
229
122
- def doWatchPolling () =
123
- doWatch(notifiablesChanged = () => watchedPathsSeq.exists(p => ! validateAnyWatchable(p)))
124
-
125
- def doWatchFsNotify () = {
126
- Using .resource(os.write.outputStream(watchArgs.serverDir / " fsNotifyWatchLog" )) { watchLog =>
127
- def writeToWatchLog (s : String ): Unit = {
128
- try {
129
- watchLog.write(s.getBytes(java.nio.charset.StandardCharsets .UTF_8 ))
130
- watchLog.write('\n ' )
131
- } catch {
132
- case _ : ClosedChannelException => /* do nothing, the file is already closed */
133
- }
134
- }
135
-
136
- @ volatile var pathChangesDetected = false
137
-
138
- // oslib watch only works with folders, so we have to watch the parent folders instead
139
-
140
- writeToWatchLog(
141
- s " [watched-paths:unfiltered] ${watchedPathsSet.toSeq.sorted.mkString(" \n " )}"
142
- )
143
-
144
- val workspaceRoot = WorkspaceRoot .workspaceRoot
145
-
146
- /** Paths that are descendants of [[workspaceRoot ]]. */
147
- val pathsUnderWorkspaceRoot = watchedPathsSet.filter { path =>
148
- val isUnderWorkspaceRoot = path.startsWith(workspaceRoot)
149
- if (! isUnderWorkspaceRoot) {
150
- streams.err.println(watchArgs.colors.error(
151
- s " Watched path $path is outside workspace root $workspaceRoot, this is unsupported. "
152
- ).toString())
153
- }
154
-
155
- isUnderWorkspaceRoot
156
- }
157
-
158
- // If I have 'root/a/b/c'
159
- //
160
- // Then I want to watch:
161
- // root/a/b/c
162
- // root/a/b
163
- // root/a
164
- // root
165
- val filterPaths = pathsUnderWorkspaceRoot.flatMap { path =>
166
- path.relativeTo(workspaceRoot).segments.inits.map(segments => workspaceRoot / segments)
167
- }
168
- writeToWatchLog(s " [watched-paths:filtered] ${filterPaths.toSeq.sorted.mkString(" \n " )}" )
169
-
170
- Using .resource(os.watch.watch(
171
- // Just watch the root folder
172
- Seq (workspaceRoot),
173
- filter = path => {
174
- val shouldBeWatched =
175
- filterPaths.contains(path) || watchedPathsSet.exists(watchedPath =>
176
- path.startsWith(watchedPath)
177
- )
178
- writeToWatchLog(s " [filter] (shouldBeWatched= $shouldBeWatched) $path" )
179
- shouldBeWatched
180
- },
181
- onEvent = changedPaths => {
182
- // Make sure that the changed paths are actually the ones in our watch list and not some adjacent files in the
183
- // same folder
184
- val hasWatchedPath =
185
- changedPaths.exists(p =>
186
- watchedPathsSet.exists(watchedPath => p.startsWith(watchedPath))
187
- )
188
- writeToWatchLog(
189
- s " [changed-paths] (hasWatchedPath= $hasWatchedPath) ${changedPaths.mkString(" \n " )}"
190
- )
191
- if (hasWatchedPath) {
192
- pathChangesDetected = true
193
- }
194
- },
195
- logger = (eventType, data) =>
196
- writeToWatchLog(s " [watch:event] $eventType: ${pprint.apply(data).plainText}" )
197
- )) { _ =>
198
- doWatch(notifiablesChanged = () => pathChangesDetected)
199
- }
200
- }
230
+ watchViaNotifyArgs match {
231
+ case Some (notifyArgs) => doWatch(notifyArgs.notifiablesChanged)
232
+ case None => doWatch(notifiablesChanged = () => watchedPathsSeq.exists(p => ! validateAnyWatchable(p)))
201
233
}
202
-
203
- if (watchArgs.useNotify) doWatchFsNotify()
204
- else doWatchPolling()
205
234
}
206
235
207
236
/**
0 commit comments