Skip to content

Commit aaed2d1

Browse files
committed
TSCUtility: implement FSWatch for Windows
Rather than simply take the no-op watcher which does not watch the file system, implement the FS Watcher using `ReadDirectoryChangesW`. This should allow SourceKit-LSP to be aware of changes to the package.resolved file.
1 parent 83dce6e commit aaed2d1

File tree

1 file changed

+174
-3
lines changed

1 file changed

+174
-3
lines changed

Sources/TSCUtility/FSWatch.swift

Lines changed: 174 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import TSCBasic
1212
import Dispatch
1313
import Foundation
1414
import TSCLibc
15+
#if os(Windows)
16+
import WinSDK
17+
#endif
1518

1619
/// FSWatch is a cross-platform filesystem watching utility.
1720
public 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)
103+
#if os(OpenBSD)
99104
extension FSWatch._WatcherDelegate: NoOpWatcherDelegate {}
100105
extension NoOpWatcher: _FileWatcher{}
106+
#elseif os(Windows)
107+
extension FSWatch._WatcherDelegate: RDCWatcherDelegate {}
108+
extension RDCWatcher: _FileWatcher {}
101109
#elseif canImport(Glibc)
102110
extension FSWatch._WatcherDelegate: InotifyDelegate {}
103111
extension Inotify: _FileWatcher{}
@@ -110,7 +118,7 @@ extension FSEventStream: _FileWatcher{}
110118

111119
// MARK:- inotify
112120

113-
#if os(OpenBSD) || os(Windows)
121+
#if os(OpenBSD)
114122

115123
public 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

Comments
 (0)