@@ -12,6 +12,9 @@ import TSCBasic
12
12
import Dispatch
13
13
import Foundation
14
14
import TSCLibc
15
+ #if os(Windows)
16
+ import WinSDK
17
+ #endif
15
18
16
19
/// FSWatch is a cross-platform filesystem watching utility.
17
20
public class FSWatch {
@@ -47,8 +50,10 @@ public class FSWatch {
47
50
self . paths = paths
48
51
self . latency = latency
49
52
50
- #if os(OpenBSD) || os(Windows)
53
+ #if os(OpenBSD)
51
54
self . _watcher = NoOpWatcher ( paths: paths, latency: latency, delegate: _WatcherDelegate ( block: block) )
55
+ #elseif os(Windows)
56
+ self . _watcher = RDCWatcher ( paths: paths, latency: latency, delegate: _WatcherDelegate ( block: block) )
52
57
#elseif canImport(Glibc)
53
58
var ipaths : [ AbsolutePath : Inotify . WatchOptions ] = [ : ]
54
59
@@ -95,9 +100,12 @@ private protocol _FileWatcher {
95
100
func stop( )
96
101
}
97
102
98
- #if os(OpenBSD) || os(Windows) || os( iOS)
103
+ #if os(OpenBSD) || os(iOS)
99
104
extension FSWatch . _WatcherDelegate : NoOpWatcherDelegate { }
100
105
extension NoOpWatcher : _FileWatcher { }
106
+ #elseif os(Windows)
107
+ extension FSWatch . _WatcherDelegate : RDCWatcherDelegate { }
108
+ extension RDCWatcher : _FileWatcher { }
101
109
#elseif canImport(Glibc)
102
110
extension FSWatch . _WatcherDelegate : InotifyDelegate { }
103
111
extension Inotify : _FileWatcher { }
@@ -110,7 +118,7 @@ extension FSEventStream: _FileWatcher{}
110
118
111
119
// MARK:- inotify
112
120
113
- #if os(OpenBSD) || os(Windows) || os( iOS)
121
+ #if os(OpenBSD) || os(iOS)
114
122
115
123
public protocol NoOpWatcherDelegate {
116
124
func pathsDidReceiveEvent( _ paths: [ AbsolutePath ] )
@@ -125,6 +133,169 @@ public final class NoOpWatcher {
125
133
public func stop( ) { }
126
134
}
127
135
136
+ #elseif os(Windows)
137
+
138
+ public protocol RDCWatcherDelegate {
139
+ func pathsDidReceiveEvent( _ paths: [ AbsolutePath ] )
140
+ }
141
+
142
+ /// Bindings for `ReadDirectoryChangesW` C APIs.
143
+ public final class RDCWatcher {
144
+ class Watch {
145
+ var hDirectory : HANDLE
146
+ let path : String
147
+ var overlapped : OVERLAPPED
148
+ var terminate : HANDLE
149
+ var buffer : UnsafeMutableBufferPointer < DWORD > // buffer must be DWORD-aligned
150
+ var thread : TSCBasic . Thread ?
151
+
152
+ public init ( directory handle: HANDLE , _ path: String ) {
153
+ self . hDirectory = handle
154
+ self . path = path
155
+ self . overlapped = OVERLAPPED ( )
156
+ self . overlapped. hEvent = CreateEventW ( nil , false , false , nil )
157
+ self . terminate = CreateEventW ( nil , true , false , nil )
158
+
159
+ let EntrySize : Int =
160
+ MemoryLayout < FILE_NOTIFY_INFORMATION > . stride + ( Int ( MAX_PATH) * MemoryLayout < WCHAR > . stride)
161
+ self . buffer =
162
+ UnsafeMutableBufferPointer< DWORD> . allocate( capacity: EntrySize * 4 / MemoryLayout< DWORD> . stride)
163
+ }
164
+
165
+ deinit {
166
+ SetEvent ( self . terminate)
167
+ CloseHandle ( self . terminate)
168
+ CloseHandle ( self . overlapped. hEvent)
169
+ CloseHandle ( hDirectory)
170
+ self . buffer. deallocate ( )
171
+ }
172
+ }
173
+
174
+ /// The paths being watched.
175
+ private let paths : [ AbsolutePath ]
176
+
177
+ /// The settle period (in seconds).
178
+ private let settle : Double
179
+
180
+ /// The watcher delegate.
181
+ private let delegate : RDCWatcherDelegate ?
182
+
183
+ private let watches : [ Watch ]
184
+ private let queue : DispatchQueue =
185
+ DispatchQueue ( label: " org.swift.swiftpm. \( RDCWatcher . self) .callback " )
186
+
187
+ public init ( paths: [ AbsolutePath ] , latency: Double , delegate: RDCWatcherDelegate ? = nil ) {
188
+ self . paths = paths
189
+ self . settle = latency
190
+ self . delegate = delegate
191
+
192
+ self . watches = paths. map {
193
+ $0. pathString. withCString ( encodedAs: UTF16 . self) {
194
+ let dwDesiredAccess : DWORD = DWORD ( FILE_LIST_DIRECTORY)
195
+ let dwShareMode : DWORD = DWORD ( FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
196
+ let dwCreationDisposition : DWORD = DWORD ( OPEN_EXISTING)
197
+ let dwFlags : DWORD = DWORD ( FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED)
198
+
199
+ let handle : HANDLE =
200
+ CreateFileW ( $0, dwDesiredAccess, dwShareMode, nil ,
201
+ dwCreationDisposition, dwFlags, nil )
202
+ assert ( !( handle == INVALID_HANDLE_VALUE) )
203
+
204
+ let dwSize : DWORD = GetFinalPathNameByHandleW ( handle, nil , 0 , 0 )
205
+ let path : String = String ( decodingCString: Array < WCHAR > ( unsafeUninitializedCapacity: Int ( dwSize) + 1 ) {
206
+ let dwSize : DWORD = GetFinalPathNameByHandleW ( handle, $0. baseAddress, DWORD ( $0. count) , 0 )
207
+ assert ( dwSize == $0. count)
208
+ $1 = Int ( dwSize)
209
+ } , as: UTF16 . self)
210
+
211
+ return Watch ( directory: handle, path)
212
+ }
213
+ }
214
+ }
215
+
216
+ public func start( ) throws {
217
+ // TODO(compnerd) can we compress the threads to a single worker thread
218
+ self . watches. forEach { watch in
219
+ watch. thread = Thread { [ delegate = self . delegate, queue = self . queue, weak watch] in
220
+ guard let watch = watch else { return }
221
+
222
+ while true {
223
+ let dwNotifyFilter : DWORD = DWORD ( FILE_NOTIFY_CHANGE_FILE_NAME)
224
+ | DWORD( FILE_NOTIFY_CHANGE_DIR_NAME)
225
+ | DWORD( FILE_NOTIFY_CHANGE_SIZE)
226
+ | DWORD( FILE_NOTIFY_CHANGE_LAST_WRITE)
227
+ | DWORD( FILE_NOTIFY_CHANGE_CREATION)
228
+ var dwBytesReturned : DWORD = 0
229
+ if !ReadDirectoryChangesW( watch. hDirectory, & watch. buffer,
230
+ DWORD ( watch. buffer. count * MemoryLayout< DWORD> . stride) ,
231
+ true , dwNotifyFilter, & dwBytesReturned,
232
+ & watch. overlapped, nil ) {
233
+ return
234
+ }
235
+
236
+ var handles : ( HANDLE ? , HANDLE ? ) = ( watch. terminate, watch. overlapped. hEvent)
237
+ switch WaitForMultipleObjects ( 2 , & handles. 0 , false , INFINITE) {
238
+ case WAIT_OBJECT_0 + 1 :
239
+ break
240
+ case DWORD ( WAIT_TIMEOUT) : // Spurious Wakeup?
241
+ continue
242
+ case WAIT_FAILED: // Failure
243
+ fallthrough
244
+ case WAIT_OBJECT_0: // Terminate Request
245
+ fallthrough
246
+ default :
247
+ CloseHandle ( watch. hDirectory)
248
+ watch. hDirectory = INVALID_HANDLE_VALUE
249
+ return
250
+ }
251
+
252
+ if !GetOverlappedResult( watch. hDirectory, & watch. overlapped, & dwBytesReturned, false ) {
253
+ queue. async {
254
+ delegate? . pathsDidReceiveEvent ( [ AbsolutePath ( watch. path) ] )
255
+ }
256
+ return
257
+ }
258
+
259
+ // There was a buffer underrun on the kernel side. We may
260
+ // have lost events, please re-synchronize.
261
+ if dwBytesReturned == 0 {
262
+ return
263
+ }
264
+
265
+ var paths : [ AbsolutePath ] = [ ]
266
+ watch. buffer. withMemoryRebound ( to: FILE_NOTIFY_INFORMATION . self) {
267
+ let pNotify : UnsafeMutablePointer < FILE_NOTIFY_INFORMATION > ? =
268
+ $0. baseAddress
269
+ while var pNotify = pNotify {
270
+ // FIXME(compnerd) do we care what type of event was received?
271
+ let file : String =
272
+ String ( utf16CodeUnitsNoCopy: & pNotify. pointee. FileName,
273
+ count: Int ( pNotify. pointee. FileNameLength) / MemoryLayout< WCHAR> . stride,
274
+ freeWhenDone: false )
275
+ paths. append ( AbsolutePath ( file) )
276
+
277
+ pNotify = ( UnsafeMutableRawPointer ( pNotify) + Int( pNotify. pointee. NextEntryOffset) )
278
+ . assumingMemoryBound ( to: FILE_NOTIFY_INFORMATION . self)
279
+ }
280
+ }
281
+
282
+ queue. async {
283
+ delegate? . pathsDidReceiveEvent ( paths)
284
+ }
285
+ }
286
+ }
287
+ watch. thread? . start ( )
288
+ }
289
+ }
290
+
291
+ public func stop( ) {
292
+ self . watches. forEach {
293
+ SetEvent ( $0. terminate)
294
+ $0. thread? . join ( )
295
+ }
296
+ }
297
+ }
298
+
128
299
#elseif canImport(Glibc)
129
300
130
301
/// The delegate for receiving inotify events.
0 commit comments