返回 DeepSeek-Reasonix
workspace_watcher_windows.go
根目录 / desktop / workspace_watcher_windows.go
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
355 lines GO