| 1 | //go:build windows |
| 2 | |
| 3 | package main |
| 4 | |
| 5 | import ( |
| 6 | "encoding/binary" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "strings" |
| 12 | "sync" |
| 13 | |
| 14 | "github.com/fsnotify/fsnotify" |
| 15 | "golang.org/x/sys/windows" |
| 16 | ) |
| 17 | |
| 18 | const ( |
| 19 | windowsWorkspaceWatchBuffer = 64 * 1024 |
| 20 | windowsWorkspaceEventBuffer = 256 |
| 21 | ) |
| 22 | |
| 23 | // Windows keeps a single recursive ReadDirectoryChangesW handle per root. |
| 24 | // Per-directory handles can prevent renaming an ancestor while a read is |
| 25 | // pending, even when each handle was opened with FILE_SHARE_DELETE (#8770). |
| 26 | type windowsWorkspaceWatcher struct { |
| 27 | mu sync.Mutex |
| 28 | events chan fsnotify.Event |
| 29 | errors chan error |
| 30 | closed chan struct{} |
| 31 | watches map[string]*windowsWorkspaceSubscription |
| 32 | isClosed bool |
| 33 | wg sync.WaitGroup |
| 34 | } |
| 35 | |
| 36 | type windowsWorkspaceSubscription struct { |
| 37 | path string |
| 38 | handle windows.Handle |
| 39 | event windows.Handle |
| 40 | stopEvent windows.Handle |
| 41 | recursive bool |
| 42 | ready chan error |
| 43 | stop chan struct{} |
| 44 | done chan struct{} |
| 45 | readyOnce sync.Once |
| 46 | stopOnce sync.Once |
| 47 | stopErr error |
| 48 | } |
| 49 | |
| 50 | func newWorkspaceWatcher() (workspaceWatcher, error) { |
| 51 | return &windowsWorkspaceWatcher{ |
| 52 | events: make(chan fsnotify.Event, windowsWorkspaceEventBuffer), |
| 53 | errors: make(chan error, 1), |
| 54 | closed: make(chan struct{}), |
| 55 | watches: make(map[string]*windowsWorkspaceSubscription), |
| 56 | }, nil |
| 57 | } |
| 58 | |
| 59 | func (w *windowsWorkspaceWatcher) Events() <-chan fsnotify.Event { return w.events } |
| 60 | func (w *windowsWorkspaceWatcher) Errors() <-chan error { return w.errors } |
| 61 | func (w *windowsWorkspaceWatcher) SupportsRecursive() bool { return true } |
| 62 | |
| 63 | func (w *windowsWorkspaceWatcher) Add(path string, recursive bool) error { |
| 64 | path = filepath.Clean(path) |
| 65 | handle, err := windows.CreateFile( |
| 66 | windows.StringToUTF16Ptr(path), |
| 67 | windows.FILE_LIST_DIRECTORY, |
| 68 | windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, |
| 69 | nil, |
| 70 | windows.OPEN_EXISTING, |
| 71 | windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED, |
| 72 | 0, |
| 73 | ) |
| 74 | if err != nil { |
| 75 | return os.NewSyscallError("CreateFile", err) |
| 76 | } |
| 77 | event, err := windows.CreateEvent(nil, 0, 0, nil) |
| 78 | if err != nil { |
| 79 | _ = windows.CloseHandle(handle) |
| 80 | return os.NewSyscallError("CreateEvent", err) |
| 81 | } |
| 82 | stopEvent, err := windows.CreateEvent(nil, 1, 0, nil) |
| 83 | if err != nil { |
| 84 | _ = windows.CloseHandle(handle) |
| 85 | _ = windows.CloseHandle(event) |
| 86 | return os.NewSyscallError("CreateEvent(stop)", err) |
| 87 | } |
| 88 | |
| 89 | key := windowsWorkspaceWatchKey(path) |
| 90 | w.mu.Lock() |
| 91 | if w.isClosed { |
| 92 | w.mu.Unlock() |
| 93 | _ = windows.CloseHandle(handle) |
| 94 | _ = windows.CloseHandle(event) |
| 95 | _ = windows.CloseHandle(stopEvent) |
| 96 | return fsnotify.ErrClosed |
| 97 | } |
| 98 | if _, exists := w.watches[key]; exists { |
| 99 | w.mu.Unlock() |
| 100 | _ = windows.CloseHandle(handle) |
| 101 | _ = windows.CloseHandle(event) |
| 102 | _ = windows.CloseHandle(stopEvent) |
| 103 | return nil |
| 104 | } |
| 105 | sub := &windowsWorkspaceSubscription{ |
| 106 | path: path, handle: handle, event: event, stopEvent: stopEvent, recursive: recursive, |
| 107 | ready: make(chan error, 1), stop: make(chan struct{}), done: make(chan struct{}), |
| 108 | } |
| 109 | w.watches[key] = sub |
| 110 | w.wg.Add(1) |
| 111 | w.mu.Unlock() |
| 112 | go w.read(sub) |
| 113 | if err := <-sub.ready; err != nil { |
| 114 | w.mu.Lock() |
| 115 | if w.watches[key] == sub { |
| 116 | delete(w.watches, key) |
| 117 | } |
| 118 | w.mu.Unlock() |
| 119 | _ = sub.close() |
| 120 | return err |
| 121 | } |
| 122 | return nil |
| 123 | } |
| 124 | |
| 125 | func (w *windowsWorkspaceWatcher) Remove(path string) error { |
| 126 | key := windowsWorkspaceWatchKey(path) |
| 127 | w.mu.Lock() |
| 128 | sub := w.watches[key] |
| 129 | delete(w.watches, key) |
| 130 | w.mu.Unlock() |
| 131 | if sub == nil { |
| 132 | return nil |
| 133 | } |
| 134 | return sub.close() |
| 135 | } |
| 136 | |
| 137 | func (w *windowsWorkspaceWatcher) Close() error { |
| 138 | w.mu.Lock() |
| 139 | if w.isClosed { |
| 140 | w.mu.Unlock() |
| 141 | return nil |
| 142 | } |
| 143 | w.isClosed = true |
| 144 | close(w.closed) |
| 145 | subs := make([]*windowsWorkspaceSubscription, 0, len(w.watches)) |
| 146 | for _, sub := range w.watches { |
| 147 | subs = append(subs, sub) |
| 148 | } |
| 149 | w.watches = make(map[string]*windowsWorkspaceSubscription) |
| 150 | w.mu.Unlock() |
| 151 | |
| 152 | var firstErr error |
| 153 | for _, sub := range subs { |
| 154 | if err := sub.close(); err != nil && firstErr == nil { |
| 155 | firstErr = err |
| 156 | } |
| 157 | } |
| 158 | w.wg.Wait() |
| 159 | close(w.events) |
| 160 | close(w.errors) |
| 161 | return firstErr |
| 162 | } |
| 163 | |
| 164 | func (s *windowsWorkspaceSubscription) close() error { |
| 165 | s.stopOnce.Do(func() { |
| 166 | close(s.stop) |
| 167 | if err := windows.SetEvent(s.stopEvent); err != nil { |
| 168 | s.stopErr = os.NewSyscallError("SetEvent(stop)", err) |
| 169 | _ = windows.CancelIoEx(s.handle, nil) |
| 170 | } |
| 171 | <-s.done |
| 172 | if err := windows.CloseHandle(s.handle); err != nil && s.stopErr == nil { |
| 173 | s.stopErr = os.NewSyscallError("CloseHandle", err) |
| 174 | } |
| 175 | if err := windows.CloseHandle(s.event); err != nil && s.stopErr == nil { |
| 176 | s.stopErr = os.NewSyscallError("CloseHandle(event)", err) |
| 177 | } |
| 178 | if err := windows.CloseHandle(s.stopEvent); err != nil && s.stopErr == nil { |
| 179 | s.stopErr = os.NewSyscallError("CloseHandle(stop)", err) |
| 180 | } |
| 181 | }) |
| 182 | return s.stopErr |
| 183 | } |
| 184 | |
| 185 | func (w *windowsWorkspaceWatcher) read(sub *windowsWorkspaceSubscription) { |
| 186 | defer w.wg.Done() |
| 187 | defer close(sub.done) |
| 188 | ready := false |
| 189 | defer func() { |
| 190 | if !ready { |
| 191 | sub.signalReady(fsnotify.ErrClosed) |
| 192 | } |
| 193 | }() |
| 194 | buf := make([]byte, windowsWorkspaceWatchBuffer) |
| 195 | mask := uint32(windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME | windows.FILE_NOTIFY_CHANGE_LAST_WRITE) |
| 196 | for { |
| 197 | if w.stopping(sub) { |
| 198 | return |
| 199 | } |
| 200 | ov := windows.Overlapped{HEvent: sub.event} |
| 201 | err := windows.ReadDirectoryChanges(sub.handle, &buf[0], uint32(len(buf)), sub.recursive, mask, nil, &ov, 0) |
| 202 | if err != nil && !errors.Is(err, windows.ERROR_IO_PENDING) { |
| 203 | if w.stopping(sub) || errors.Is(err, windows.ERROR_OPERATION_ABORTED) || errors.Is(err, windows.ERROR_INVALID_HANDLE) { |
| 204 | return |
| 205 | } |
| 206 | if !ready { |
| 207 | sub.signalReady(os.NewSyscallError("ReadDirectoryChanges", err)) |
| 208 | ready = true |
| 209 | return |
| 210 | } |
| 211 | if errors.Is(err, windows.ERROR_NOTIFY_ENUM_DIR) { |
| 212 | w.sendError(sub, fsnotify.ErrEventOverflow) |
| 213 | continue |
| 214 | } |
| 215 | w.sendError(sub, os.NewSyscallError("ReadDirectoryChanges", err)) |
| 216 | return |
| 217 | } |
| 218 | if !ready { |
| 219 | sub.signalReady(nil) |
| 220 | ready = true |
| 221 | } |
| 222 | which, err := windows.WaitForMultipleObjects([]windows.Handle{sub.event, sub.stopEvent}, false, windows.INFINITE) |
| 223 | if err != nil { |
| 224 | if w.stopping(sub) { |
| 225 | return |
| 226 | } |
| 227 | w.sendError(sub, os.NewSyscallError("WaitForMultipleObjects", err)) |
| 228 | return |
| 229 | } |
| 230 | if which == windows.WAIT_OBJECT_0+1 { |
| 231 | cancelErr := windows.CancelIoEx(sub.handle, &ov) |
| 232 | if cancelErr != nil && !errors.Is(cancelErr, windows.ERROR_NOT_FOUND) { |
| 233 | w.sendError(sub, os.NewSyscallError("CancelIoEx", cancelErr)) |
| 234 | } |
| 235 | var ignored uint32 |
| 236 | if err := windows.GetOverlappedResult(sub.handle, &ov, &ignored, true); err != nil && !errors.Is(err, windows.ERROR_OPERATION_ABORTED) { |
| 237 | w.sendError(sub, os.NewSyscallError("GetOverlappedResult(cancel)", err)) |
| 238 | } |
| 239 | return |
| 240 | } |
| 241 | if which != windows.WAIT_OBJECT_0 { |
| 242 | w.sendError(sub, fmt.Errorf("WaitForMultipleObjects: unexpected result %d", which)) |
| 243 | return |
| 244 | } |
| 245 | var n uint32 |
| 246 | if err := windows.GetOverlappedResult(sub.handle, &ov, &n, false); err != nil { |
| 247 | if w.stopping(sub) || errors.Is(err, windows.ERROR_OPERATION_ABORTED) || errors.Is(err, windows.ERROR_INVALID_HANDLE) { |
| 248 | return |
| 249 | } |
| 250 | if errors.Is(err, windows.ERROR_NOTIFY_ENUM_DIR) || errors.Is(err, windows.ERROR_MORE_DATA) { |
| 251 | w.sendError(sub, fsnotify.ErrEventOverflow) |
| 252 | continue |
| 253 | } |
| 254 | w.sendError(sub, os.NewSyscallError("GetOverlappedResult", err)) |
| 255 | return |
| 256 | } |
| 257 | if n == 0 { |
| 258 | w.sendError(sub, fsnotify.ErrEventOverflow) |
| 259 | continue |
| 260 | } |
| 261 | if err := w.publishBuffer(sub, buf[:n]); err != nil { |
| 262 | w.sendError(sub, err) |
| 263 | } |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | func (s *windowsWorkspaceSubscription) signalReady(err error) { |
| 268 | s.readyOnce.Do(func() { s.ready <- err }) |
| 269 | } |
| 270 | |
| 271 | func (w *windowsWorkspaceWatcher) publishBuffer(sub *windowsWorkspaceSubscription, buf []byte) error { |
| 272 | const header = 12 |
| 273 | for offset := 0; ; { |
| 274 | if len(buf)-offset < header { |
| 275 | return fmt.Errorf("ReadDirectoryChanges: malformed notification header") |
| 276 | } |
| 277 | next := int(binary.LittleEndian.Uint32(buf[offset:])) |
| 278 | action := binary.LittleEndian.Uint32(buf[offset+4:]) |
| 279 | nameBytes := int(binary.LittleEndian.Uint32(buf[offset+8:])) |
| 280 | if nameBytes < 0 || nameBytes%2 != 0 || nameBytes > len(buf)-offset-header { |
| 281 | return fmt.Errorf("ReadDirectoryChanges: malformed notification name") |
| 282 | } |
| 283 | units := make([]uint16, nameBytes/2) |
| 284 | for i := range units { |
| 285 | start := offset + header + i*2 |
| 286 | units[i] = binary.LittleEndian.Uint16(buf[start:]) |
| 287 | } |
| 288 | name := windows.UTF16ToString(units) |
| 289 | if op := windowsWorkspaceOp(action); op != 0 && name != "" { |
| 290 | if !w.sendEvent(sub, fsnotify.Event{Name: filepath.Join(sub.path, name), Op: op}) { |
| 291 | return nil |
| 292 | } |
| 293 | } |
| 294 | if next == 0 { |
| 295 | return nil |
| 296 | } |
| 297 | if next < header || next > len(buf)-offset { |
| 298 | return fmt.Errorf("ReadDirectoryChanges: invalid notification offset") |
| 299 | } |
| 300 | offset += next |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | func windowsWorkspaceOp(action uint32) fsnotify.Op { |
| 305 | switch action { |
| 306 | case windows.FILE_ACTION_ADDED, windows.FILE_ACTION_RENAMED_NEW_NAME: |
| 307 | return fsnotify.Create |
| 308 | case windows.FILE_ACTION_REMOVED: |
| 309 | return fsnotify.Remove |
| 310 | case windows.FILE_ACTION_MODIFIED: |
| 311 | return fsnotify.Write |
| 312 | case windows.FILE_ACTION_RENAMED_OLD_NAME: |
| 313 | return fsnotify.Rename |
| 314 | default: |
| 315 | return 0 |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | func (w *windowsWorkspaceWatcher) sendEvent(sub *windowsWorkspaceSubscription, ev fsnotify.Event) bool { |
| 320 | select { |
| 321 | case <-w.closed: |
| 322 | return false |
| 323 | case <-sub.stop: |
| 324 | return false |
| 325 | case w.events <- ev: |
| 326 | return true |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | func (w *windowsWorkspaceWatcher) sendError(sub *windowsWorkspaceSubscription, err error) bool { |
| 331 | select { |
| 332 | case <-w.closed: |
| 333 | return false |
| 334 | case <-sub.stop: |
| 335 | return false |
| 336 | case w.errors <- err: |
| 337 | return true |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | func (w *windowsWorkspaceWatcher) stopping(sub *windowsWorkspaceSubscription) bool { |
| 342 | select { |
| 343 | case <-w.closed: |
| 344 | return true |
| 345 | case <-sub.stop: |
| 346 | return true |
| 347 | default: |
| 348 | return false |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | func windowsWorkspaceWatchKey(path string) string { |
| 353 | return strings.ToLower(filepath.Clean(path)) |
| 354 | } |
| 355 |