1//go:build solaris
2
3// FEN backend for illumos (supported) and Solaris (untested, but should work).
4//
5// See port_create(3c) etc. for docs. https://www.illumos.org/man/3C/port_create
6
7package fsnotify
8
9import (
10 "errors"
11 "fmt"
12 "os"
13 "path/filepath"
14 "sync"
15 "time"
16
17 "github.com/fsnotify/fsnotify/internal"
18 "golang.org/x/sys/unix"
19)
20
21type fen struct {
22 Events chan Event
23 Errors chan error
24
25 mu sync.Mutex
26 port *unix.EventPort
27 done chan struct{} // Channel for sending a "quit message" to the reader goroutine
28 dirs map[string]Op // Explicitly watched directories
29 watches map[string]Op // Explicitly watched non-directories
30}
31
32func newBackend(ev chan Event, errs chan error) (backend, error) {
33 return newBufferedBackend(0, ev, errs)
34}
35
36func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
37 w := &fen{
38 Events: ev,
39 Errors: errs,
40 dirs: make(map[string]Op),
41 watches: make(map[string]Op),
42 done: make(chan struct{}),
43 }
44
45 var err error
46 w.port, err = unix.NewEventPort()
47 if err != nil {
48 return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err)
49 }
50
51 go w.readEvents()
52 return w, nil
53}
54
55// sendEvent attempts to send an event to the user, returning true if the event
56// was put in the channel successfully and false if the watcher has been closed.
57func (w *fen) sendEvent(name string, op Op) (sent bool) {
58 select {
59 case <-w.done:
60 return false
61 case w.Events <- Event{Name: name, Op: op}:
62 return true
63 }
64}
65
66// sendError attempts to send an error to the user, returning true if the error
67// was put in the channel successfully and false if the watcher has been closed.
68func (w *fen) sendError(err error) (sent bool) {
69 if err == nil {
70 return true
71 }
72 select {
73 case <-w.done:
74 return false
75 case w.Errors <- err:
76 return true
77 }
78}
79
80func (w *fen) isClosed() bool {
81 select {
82 case <-w.done:
83 return true
84 default:
85 return false
86 }
87}
88
89func (w *fen) Close() error {
90 // Take the lock used by associateFile to prevent lingering events from
91 // being processed after the close
92 w.mu.Lock()
93 defer w.mu.Unlock()
94 if w.isClosed() {
95 return nil
96 }
97 close(w.done)
98 return w.port.Close()
99}
100
101func (w *fen) Add(name string) error { return w.AddWith(name) }
102
103func (w *fen) AddWith(name string, opts ...addOpt) error {
104 if w.isClosed() {
105 return ErrClosed
106 }
107 if debug {
108 fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n",
109 time.Now().Format("15:04:05.000000000"), name)
110 }
111
112 with := getOptions(opts...)
113 if !w.xSupports(with.op) {
114 return fmt.Errorf("%w: %s", xErrUnsupported, with.op)
115 }
116
117 // Currently we resolve symlinks that were explicitly requested to be
118 // watched. Otherwise we would use LStat here.
119 stat, err := os.Stat(name)
120 if err != nil {
121 return err
122 }
123
124 // Associate all files in the directory.
125 if stat.IsDir() {
126 err := w.handleDirectory(name, stat, true, w.associateFile)
127 if err != nil {
128 return err
129 }
130
131 w.mu.Lock()
132 w.dirs[name] = with.op
133 w.mu.Unlock()
134 return nil
135 }
136
137 err = w.associateFile(name, stat, true)
138 if err != nil {
139 return err
140 }
141
142 w.mu.Lock()
143 w.watches[name] = with.op
144 w.mu.Unlock()
145 return nil
146}
147
148func (w *fen) Remove(name string) error {
149 if w.isClosed() {
150 return nil
151 }
152 if !w.port.PathIsWatched(name) {
153 return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
154 }
155 if debug {
156 fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n",
157 time.Now().Format("15:04:05.000000000"), name)
158 }
159
160 // The user has expressed an intent. Immediately remove this name from
161 // whichever watch list it might be in. If it's not in there the delete
162 // doesn't cause harm.
163 w.mu.Lock()
164 delete(w.watches, name)
165 delete(w.dirs, name)
166 w.mu.Unlock()
167
168 stat, err := os.Stat(name)
169 if err != nil {
170 return err
171 }
172
173 // Remove associations for every file in the directory.
174 if stat.IsDir() {
175 err := w.handleDirectory(name, stat, false, w.dissociateFile)
176 if err != nil {
177 return err
178 }
179 return nil
180 }
181
182 err = w.port.DissociatePath(name)
183 if err != nil {
184 return err
185 }
186
187 return nil
188}
189
190// readEvents contains the main loop that runs in a goroutine watching for events.
191func (w *fen) readEvents() {
192 // If this function returns, the watcher has been closed and we can close
193 // these channels
194 defer func() {
195 close(w.Errors)
196 close(w.Events)
197 }()
198
199 pevents := make([]unix.PortEvent, 8)
200 for {
201 count, err := w.port.Get(pevents, 1, nil)
202 if err != nil && err != unix.ETIME {
203 // Interrupted system call (count should be 0) ignore and continue
204 if errors.Is(err, unix.EINTR) && count == 0 {
205 continue
206 }
207 // Get failed because we called w.Close()
208 if errors.Is(err, unix.EBADF) && w.isClosed() {
209 return
210 }
211 // There was an error not caused by calling w.Close()
212 if !w.sendError(err) {
213 return
214 }
215 }
216
217 p := pevents[:count]
218 for _, pevent := range p {
219 if pevent.Source != unix.PORT_SOURCE_FILE {
220 // Event from unexpected source received; should never happen.
221 if !w.sendError(errors.New("Event from unexpected source received")) {
222 return
223 }
224 continue
225 }
226
227 if debug {
228 internal.Debug(pevent.Path, pevent.Events)
229 }
230
231 err = w.handleEvent(&pevent)
232 if !w.sendError(err) {
233 return
234 }
235 }
236 }
237}
238
239func (w *fen) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
240 files, err := os.ReadDir(path)
241 if err != nil {
242 return err
243 }
244
245 // Handle all children of the directory.
246 for _, entry := range files {
247 finfo, err := entry.Info()
248 if err != nil {
249 return err
250 }
251 err = handler(filepath.Join(path, finfo.Name()), finfo, false)
252 if err != nil {
253 return err
254 }
255 }
256
257 // And finally handle the directory itself.
258 return handler(path, stat, follow)
259}
260
261// handleEvent might need to emit more than one fsnotify event if the events
262// bitmap matches more than one event type (e.g. the file was both modified and
263// had the attributes changed between when the association was created and the
264// when event was returned)
265func (w *fen) handleEvent(event *unix.PortEvent) error {
266 var (
267 events = event.Events
268 path = event.Path
269 fmode = event.Cookie.(os.FileMode)
270 reRegister = true
271 )
272
273 w.mu.Lock()
274 _, watchedDir := w.dirs[path]
275 _, watchedPath := w.watches[path]
276 w.mu.Unlock()
277 isWatched := watchedDir || watchedPath
278
279 if events&unix.FILE_DELETE != 0 {
280 if !w.sendEvent(path, Remove) {
281 return nil
282 }
283 reRegister = false
284 }
285 if events&unix.FILE_RENAME_FROM != 0 {
286 if !w.sendEvent(path, Rename) {
287 return nil
288 }
289 // Don't keep watching the new file name
290 reRegister = false
291 }
292 if events&unix.FILE_RENAME_TO != 0 {
293 // We don't report a Rename event for this case, because Rename events
294 // are interpreted as referring to the _old_ name of the file, and in
295 // this case the event would refer to the new name of the file. This
296 // type of rename event is not supported by fsnotify.
297
298 // inotify reports a Remove event in this case, so we simulate this
299 // here.
300 if !w.sendEvent(path, Remove) {
301 return nil
302 }
303 // Don't keep watching the file that was removed
304 reRegister = false
305 }
306
307 // The file is gone, nothing left to do.
308 if !reRegister {
309 if watchedDir {
310 w.mu.Lock()
311 delete(w.dirs, path)
312 w.mu.Unlock()
313 }
314 if watchedPath {
315 w.mu.Lock()
316 delete(w.watches, path)
317 w.mu.Unlock()
318 }
319 return nil
320 }
321
322 // If we didn't get a deletion the file still exists and we're going to have
323 // to watch it again. Let's Stat it now so that we can compare permissions
324 // and have what we need to continue watching the file
325
326 stat, err := os.Lstat(path)
327 if err != nil {
328 // This is unexpected, but we should still emit an event. This happens
329 // most often on "rm -r" of a subdirectory inside a watched directory We
330 // get a modify event of something happening inside, but by the time we
331 // get here, the sudirectory is already gone. Clearly we were watching
332 // this path but now it is gone. Let's tell the user that it was
333 // removed.
334 if !w.sendEvent(path, Remove) {
335 return nil
336 }
337 // Suppress extra write events on removed directories; they are not
338 // informative and can be confusing.
339 return nil
340 }
341
342 // resolve symlinks that were explicitly watched as we would have at Add()
343 // time. this helps suppress spurious Chmod events on watched symlinks
344 if isWatched {
345 stat, err = os.Stat(path)
346 if err != nil {
347 // The symlink still exists, but the target is gone. Report the
348 // Remove similar to above.
349 if !w.sendEvent(path, Remove) {
350 return nil
351 }
352 // Don't return the error
353 }
354 }
355
356 if events&unix.FILE_MODIFIED != 0 {
357 if fmode.IsDir() && watchedDir {
358 if err := w.updateDirectory(path); err != nil {
359 return err
360 }
361 } else {
362 if !w.sendEvent(path, Write) {
363 return nil
364 }
365 }
366 }
367 if events&unix.FILE_ATTRIB != 0 && stat != nil {
368 // Only send Chmod if perms changed
369 if stat.Mode().Perm() != fmode.Perm() {
370 if !w.sendEvent(path, Chmod) {
371 return nil
372 }
373 }
374 }
375
376 if stat != nil {
377 // If we get here, it means we've hit an event above that requires us to
378 // continue watching the file or directory
379 return w.associateFile(path, stat, isWatched)
380 }
381 return nil
382}
383
384func (w *fen) updateDirectory(path string) error {
385 // The directory was modified, so we must find unwatched entities and watch
386 // them. If something was removed from the directory, nothing will happen,
387 // as everything else should still be watched.
388 files, err := os.ReadDir(path)
389 if err != nil {
390 return err
391 }
392
393 for _, entry := range files {
394 path := filepath.Join(path, entry.Name())
395 if w.port.PathIsWatched(path) {
396 continue
397 }
398
399 finfo, err := entry.Info()
400 if err != nil {
401 return err
402 }
403 err = w.associateFile(path, finfo, false)
404 if !w.sendError(err) {
405 return nil
406 }
407 if !w.sendEvent(path, Create) {
408 return nil
409 }
410 }
411 return nil
412}
413
414func (w *fen) associateFile(path string, stat os.FileInfo, follow bool) error {
415 if w.isClosed() {
416 return ErrClosed
417 }
418 // This is primarily protecting the call to AssociatePath but it is
419 // important and intentional that the call to PathIsWatched is also
420 // protected by this mutex. Without this mutex, AssociatePath has been seen
421 // to error out that the path is already associated.
422 w.mu.Lock()
423 defer w.mu.Unlock()
424
425 if w.port.PathIsWatched(path) {
426 // Remove the old association in favor of this one If we get ENOENT,
427 // then while the x/sys/unix wrapper still thought that this path was
428 // associated, the underlying event port did not. This call will have
429 // cleared up that discrepancy. The most likely cause is that the event
430 // has fired but we haven't processed it yet.
431 err := w.port.DissociatePath(path)
432 if err != nil && !errors.Is(err, unix.ENOENT) {
433 return err
434 }
435 }
436
437 var events int
438 if !follow {
439 // Watch symlinks themselves rather than their targets unless this entry
440 // is explicitly watched.
441 events |= unix.FILE_NOFOLLOW
442 }
443 if true { // TODO: implement withOps()
444 events |= unix.FILE_MODIFIED
445 }
446 if true {
447 events |= unix.FILE_ATTRIB
448 }
449 return w.port.AssociatePath(path, stat, events, stat.Mode())
450}
451
452func (w *fen) dissociateFile(path string, stat os.FileInfo, unused bool) error {
453 if !w.port.PathIsWatched(path) {
454 return nil
455 }
456 return w.port.DissociatePath(path)
457}
458
459func (w *fen) WatchList() []string {
460 if w.isClosed() {
461 return nil
462 }
463
464 w.mu.Lock()
465 defer w.mu.Unlock()
466
467 entries := make([]string, 0, len(w.watches)+len(w.dirs))
468 for pathname := range w.dirs {
469 entries = append(entries, pathname)
470 }
471 for pathname := range w.watches {
472 entries = append(entries, pathname)
473 }
474
475 return entries
476}
477
478func (w *fen) xSupports(op Op) bool {
479 if op.Has(xUnportableOpen) || op.Has(xUnportableRead) ||
480 op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) {
481 return false
482 }
483 return true
484}