@@ -12,6 +12,9 @@ import TSCBasic
1212import Dispatch
1313import Foundation
1414import TSCLibc
15+ #if os(Windows)
16+ import WinSDK
17+ #endif
1518
1619/// FSWatch is a cross-platform filesystem watching utility.
1720public class FSWatch {
@@ -47,8 +50,10 @@ public class FSWatch {
4750 self . paths = paths
4851 self . latency = latency
4952
50- #if os(OpenBSD) || os(Windows)
53+ #if os(OpenBSD)
5154 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) )
5257 #elseif canImport(Glibc)
5358 var ipaths : [ AbsolutePath : Inotify . WatchOptions ] = [ : ]
5459
@@ -95,9 +100,12 @@ private protocol _FileWatcher {
95100 func stop( )
96101}
97102
98- #if os(OpenBSD) || os(Windows) || os( iOS)
103+ #if os(OpenBSD) || os(iOS)
99104extension FSWatch . _WatcherDelegate : NoOpWatcherDelegate { }
100105extension NoOpWatcher : _FileWatcher { }
106+ #elseif os(Windows)
107+ extension FSWatch . _WatcherDelegate : RDCWatcherDelegate { }
108+ extension RDCWatcher : _FileWatcher { }
101109#elseif canImport(Glibc)
102110extension FSWatch . _WatcherDelegate : InotifyDelegate { }
103111extension Inotify : _FileWatcher { }
@@ -110,7 +118,7 @@ extension FSEventStream: _FileWatcher{}
110118
111119// MARK:- inotify
112120
113- #if os(OpenBSD) || os(Windows) || os( iOS)
121+ #if os(OpenBSD) || os(iOS)
114122
115123public protocol NoOpWatcherDelegate {
116124 func pathsDidReceiveEvent( _ paths: [ AbsolutePath ] )
@@ -125,6 +133,169 @@ public final class NoOpWatcher {
125133 public func stop( ) { }
126134}
127135
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+
128299#elseif canImport(Glibc)
129300
130301/// The delegate for receiving inotify events.
0 commit comments