forked from ebhomengo/niki
641 lines
19 KiB
Go
641 lines
19 KiB
Go
//go:build solaris
|
|
// +build solaris
|
|
|
|
// Note: the documentation on the Watcher type and methods is generated from
|
|
// mkdoc.zsh
|
|
|
|
package fsnotify
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// Watcher watches a set of paths, delivering events on a channel.
|
|
//
|
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by
|
|
// value).
|
|
//
|
|
// # Linux notes
|
|
//
|
|
// When a file is removed a Remove event won't be emitted until all file
|
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
|
//
|
|
// fp := os.Open("file")
|
|
// os.Remove("file") // Triggers Chmod
|
|
// fp.Close() // Triggers Remove
|
|
//
|
|
// This is the event that inotify sends, so not much can be changed about this.
|
|
//
|
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
|
|
// for the number of watches per user, and fs.inotify.max_user_instances
|
|
// specifies the maximum number of inotify instances per user. Every Watcher you
|
|
// create is an "instance", and every path you add is a "watch".
|
|
//
|
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
|
|
// /proc/sys/fs/inotify/max_user_instances
|
|
//
|
|
// To increase them you can use sysctl or write the value to the /proc file:
|
|
//
|
|
// # Default values on Linux 5.18
|
|
// sysctl fs.inotify.max_user_watches=124983
|
|
// sysctl fs.inotify.max_user_instances=128
|
|
//
|
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
|
// your distro's documentation):
|
|
//
|
|
// fs.inotify.max_user_watches=124983
|
|
// fs.inotify.max_user_instances=128
|
|
//
|
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
|
// files" error.
|
|
//
|
|
// # kqueue notes (macOS, BSD)
|
|
//
|
|
// kqueue requires opening a file descriptor for every file that's being watched;
|
|
// so if you're watching a directory with five files then that's six file
|
|
// descriptors. You will run in to your system's "max open files" limit faster on
|
|
// these platforms.
|
|
//
|
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
|
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
|
// systems.
|
|
//
|
|
// # Windows notes
|
|
//
|
|
// Paths can be added as "C:\path\to\dir", but forward slashes
|
|
// ("C:/path/to/dir") will also work.
|
|
//
|
|
// When a watched directory is removed it will always send an event for the
|
|
// directory itself, but may not send events for all files in that directory.
|
|
// Sometimes it will send events for all times, sometimes it will send no
|
|
// events, and often only for some files.
|
|
//
|
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
|
// events in quick succession this may not be enough, and you will have to use
|
|
// [WithBufferSize] to increase the value.
|
|
type Watcher struct {
|
|
// Events sends the filesystem change events.
|
|
//
|
|
// fsnotify can send the following events; a "path" here can refer to a
|
|
// file, directory, symbolic link, or special file like a FIFO.
|
|
//
|
|
// fsnotify.Create A new path was created; this may be followed by one
|
|
// or more Write events if data also gets written to a
|
|
// file.
|
|
//
|
|
// fsnotify.Remove A path was removed.
|
|
//
|
|
// fsnotify.Rename A path was renamed. A rename is always sent with the
|
|
// old path as Event.Name, and a Create event will be
|
|
// sent with the new name. Renames are only sent for
|
|
// paths that are currently watched; e.g. moving an
|
|
// unmonitored file into a monitored directory will
|
|
// show up as just a Create. Similarly, renaming a file
|
|
// to outside a monitored directory will show up as
|
|
// only a Rename.
|
|
//
|
|
// fsnotify.Write A file or named pipe was written to. A Truncate will
|
|
// also trigger a Write. A single "write action"
|
|
// initiated by the user may show up as one or multiple
|
|
// writes, depending on when the system syncs things to
|
|
// disk. For example when compiling a large Go program
|
|
// you may get hundreds of Write events, and you may
|
|
// want to wait until you've stopped receiving them
|
|
// (see the dedup example in cmd/fsnotify).
|
|
//
|
|
// Some systems may send Write event for directories
|
|
// when the directory content changes.
|
|
//
|
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
|
// when a file is removed (or more accurately, when a
|
|
// link to an inode is removed). On kqueue it's sent
|
|
// when a file is truncated. On Windows it's never
|
|
// sent.
|
|
Events chan Event
|
|
|
|
// Errors sends any errors.
|
|
//
|
|
// ErrEventOverflow is used to indicate there are too many events:
|
|
//
|
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
|
// - kqueue, fen: Not used.
|
|
Errors chan error
|
|
|
|
mu sync.Mutex
|
|
port *unix.EventPort
|
|
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
|
dirs map[string]struct{} // Explicitly watched directories
|
|
watches map[string]struct{} // Explicitly watched non-directories
|
|
}
|
|
|
|
// NewWatcher creates a new Watcher.
|
|
func NewWatcher() (*Watcher, error) {
|
|
return NewBufferedWatcher(0)
|
|
}
|
|
|
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
|
// channel.
|
|
//
|
|
// The main use case for this is situations with a very large number of events
|
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
|
// cases, and whenever possible you will be better off increasing the kernel
|
|
// buffers instead of adding a large userspace buffer.
|
|
func NewBufferedWatcher(sz uint) (*Watcher, error) {
|
|
w := &Watcher{
|
|
Events: make(chan Event, sz),
|
|
Errors: make(chan error),
|
|
dirs: make(map[string]struct{}),
|
|
watches: make(map[string]struct{}),
|
|
done: make(chan struct{}),
|
|
}
|
|
|
|
var err error
|
|
w.port, err = unix.NewEventPort()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err)
|
|
}
|
|
|
|
go w.readEvents()
|
|
return w, nil
|
|
}
|
|
|
|
// sendEvent attempts to send an event to the user, returning true if the event
|
|
// was put in the channel successfully and false if the watcher has been closed.
|
|
func (w *Watcher) sendEvent(name string, op Op) (sent bool) {
|
|
select {
|
|
case w.Events <- Event{Name: name, Op: op}:
|
|
return true
|
|
case <-w.done:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// sendError attempts to send an error to the user, returning true if the error
|
|
// was put in the channel successfully and false if the watcher has been closed.
|
|
func (w *Watcher) sendError(err error) (sent bool) {
|
|
select {
|
|
case w.Errors <- err:
|
|
return true
|
|
case <-w.done:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (w *Watcher) isClosed() bool {
|
|
select {
|
|
case <-w.done:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Close removes all watches and closes the Events channel.
|
|
func (w *Watcher) Close() error {
|
|
// Take the lock used by associateFile to prevent lingering events from
|
|
// being processed after the close
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if w.isClosed() {
|
|
return nil
|
|
}
|
|
close(w.done)
|
|
return w.port.Close()
|
|
}
|
|
|
|
// Add starts monitoring the path for changes.
|
|
//
|
|
// A path can only be watched once; watching it more than once is a no-op and will
|
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
|
// watched.
|
|
//
|
|
// A watch will be automatically removed if the watched path is deleted or
|
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
|
// watcher on renames.
|
|
//
|
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
|
//
|
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
|
//
|
|
// See [Watcher.AddWith] for a version that allows adding options.
|
|
//
|
|
// # Watching directories
|
|
//
|
|
// All files in a directory are monitored, including new files that are created
|
|
// after the watcher is started. Subdirectories are not watched (i.e. it's
|
|
// non-recursive).
|
|
//
|
|
// # Watching files
|
|
//
|
|
// Watching individual files (rather than directories) is generally not
|
|
// recommended as many programs (especially editors) update files atomically: it
|
|
// will write to a temporary file which is then moved to to destination,
|
|
// overwriting the original (or some variant thereof). The watcher on the
|
|
// original file is now lost, as that no longer exists.
|
|
//
|
|
// The upshot of this is that a power failure or crash won't leave a
|
|
// half-written file.
|
|
//
|
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
|
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
|
|
|
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
|
// the defaults described below are used.
|
|
//
|
|
// Possible options are:
|
|
//
|
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
|
// other platforms. The default is 64K (65536 bytes).
|
|
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
|
|
if w.isClosed() {
|
|
return ErrClosed
|
|
}
|
|
if w.port.PathIsWatched(name) {
|
|
return nil
|
|
}
|
|
|
|
_ = getOptions(opts...)
|
|
|
|
// Currently we resolve symlinks that were explicitly requested to be
|
|
// watched. Otherwise we would use LStat here.
|
|
stat, err := os.Stat(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Associate all files in the directory.
|
|
if stat.IsDir() {
|
|
err := w.handleDirectory(name, stat, true, w.associateFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.mu.Lock()
|
|
w.dirs[name] = struct{}{}
|
|
w.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
err = w.associateFile(name, stat, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.mu.Lock()
|
|
w.watches[name] = struct{}{}
|
|
w.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// Remove stops monitoring the path for changes.
|
|
//
|
|
// Directories are always removed non-recursively. For example, if you added
|
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
|
//
|
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
|
//
|
|
// Returns nil if [Watcher.Close] was called.
|
|
func (w *Watcher) Remove(name string) error {
|
|
if w.isClosed() {
|
|
return nil
|
|
}
|
|
if !w.port.PathIsWatched(name) {
|
|
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
|
|
}
|
|
|
|
// The user has expressed an intent. Immediately remove this name from
|
|
// whichever watch list it might be in. If it's not in there the delete
|
|
// doesn't cause harm.
|
|
w.mu.Lock()
|
|
delete(w.watches, name)
|
|
delete(w.dirs, name)
|
|
w.mu.Unlock()
|
|
|
|
stat, err := os.Stat(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Remove associations for every file in the directory.
|
|
if stat.IsDir() {
|
|
err := w.handleDirectory(name, stat, false, w.dissociateFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
err = w.port.DissociatePath(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// readEvents contains the main loop that runs in a goroutine watching for events.
|
|
func (w *Watcher) readEvents() {
|
|
// If this function returns, the watcher has been closed and we can close
|
|
// these channels
|
|
defer func() {
|
|
close(w.Errors)
|
|
close(w.Events)
|
|
}()
|
|
|
|
pevents := make([]unix.PortEvent, 8)
|
|
for {
|
|
count, err := w.port.Get(pevents, 1, nil)
|
|
if err != nil && err != unix.ETIME {
|
|
// Interrupted system call (count should be 0) ignore and continue
|
|
if errors.Is(err, unix.EINTR) && count == 0 {
|
|
continue
|
|
}
|
|
// Get failed because we called w.Close()
|
|
if errors.Is(err, unix.EBADF) && w.isClosed() {
|
|
return
|
|
}
|
|
// There was an error not caused by calling w.Close()
|
|
if !w.sendError(err) {
|
|
return
|
|
}
|
|
}
|
|
|
|
p := pevents[:count]
|
|
for _, pevent := range p {
|
|
if pevent.Source != unix.PORT_SOURCE_FILE {
|
|
// Event from unexpected source received; should never happen.
|
|
if !w.sendError(errors.New("Event from unexpected source received")) {
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
|
|
err = w.handleEvent(&pevent)
|
|
if err != nil {
|
|
if !w.sendError(err) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
|
|
files, err := os.ReadDir(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Handle all children of the directory.
|
|
for _, entry := range files {
|
|
finfo, err := entry.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = handler(filepath.Join(path, finfo.Name()), finfo, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// And finally handle the directory itself.
|
|
return handler(path, stat, follow)
|
|
}
|
|
|
|
// handleEvent might need to emit more than one fsnotify event if the events
|
|
// bitmap matches more than one event type (e.g. the file was both modified and
|
|
// had the attributes changed between when the association was created and the
|
|
// when event was returned)
|
|
func (w *Watcher) handleEvent(event *unix.PortEvent) error {
|
|
var (
|
|
events = event.Events
|
|
path = event.Path
|
|
fmode = event.Cookie.(os.FileMode)
|
|
reRegister = true
|
|
)
|
|
|
|
w.mu.Lock()
|
|
_, watchedDir := w.dirs[path]
|
|
_, watchedPath := w.watches[path]
|
|
w.mu.Unlock()
|
|
isWatched := watchedDir || watchedPath
|
|
|
|
if events&unix.FILE_DELETE != 0 {
|
|
if !w.sendEvent(path, Remove) {
|
|
return nil
|
|
}
|
|
reRegister = false
|
|
}
|
|
if events&unix.FILE_RENAME_FROM != 0 {
|
|
if !w.sendEvent(path, Rename) {
|
|
return nil
|
|
}
|
|
// Don't keep watching the new file name
|
|
reRegister = false
|
|
}
|
|
if events&unix.FILE_RENAME_TO != 0 {
|
|
// We don't report a Rename event for this case, because Rename events
|
|
// are interpreted as referring to the _old_ name of the file, and in
|
|
// this case the event would refer to the new name of the file. This
|
|
// type of rename event is not supported by fsnotify.
|
|
|
|
// inotify reports a Remove event in this case, so we simulate this
|
|
// here.
|
|
if !w.sendEvent(path, Remove) {
|
|
return nil
|
|
}
|
|
// Don't keep watching the file that was removed
|
|
reRegister = false
|
|
}
|
|
|
|
// The file is gone, nothing left to do.
|
|
if !reRegister {
|
|
if watchedDir {
|
|
w.mu.Lock()
|
|
delete(w.dirs, path)
|
|
w.mu.Unlock()
|
|
}
|
|
if watchedPath {
|
|
w.mu.Lock()
|
|
delete(w.watches, path)
|
|
w.mu.Unlock()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// If we didn't get a deletion the file still exists and we're going to have
|
|
// to watch it again. Let's Stat it now so that we can compare permissions
|
|
// and have what we need to continue watching the file
|
|
|
|
stat, err := os.Lstat(path)
|
|
if err != nil {
|
|
// This is unexpected, but we should still emit an event. This happens
|
|
// most often on "rm -r" of a subdirectory inside a watched directory We
|
|
// get a modify event of something happening inside, but by the time we
|
|
// get here, the sudirectory is already gone. Clearly we were watching
|
|
// this path but now it is gone. Let's tell the user that it was
|
|
// removed.
|
|
if !w.sendEvent(path, Remove) {
|
|
return nil
|
|
}
|
|
// Suppress extra write events on removed directories; they are not
|
|
// informative and can be confusing.
|
|
return nil
|
|
}
|
|
|
|
// resolve symlinks that were explicitly watched as we would have at Add()
|
|
// time. this helps suppress spurious Chmod events on watched symlinks
|
|
if isWatched {
|
|
stat, err = os.Stat(path)
|
|
if err != nil {
|
|
// The symlink still exists, but the target is gone. Report the
|
|
// Remove similar to above.
|
|
if !w.sendEvent(path, Remove) {
|
|
return nil
|
|
}
|
|
// Don't return the error
|
|
}
|
|
}
|
|
|
|
if events&unix.FILE_MODIFIED != 0 {
|
|
if fmode.IsDir() {
|
|
if watchedDir {
|
|
if err := w.updateDirectory(path); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if !w.sendEvent(path, Write) {
|
|
return nil
|
|
}
|
|
}
|
|
} else {
|
|
if !w.sendEvent(path, Write) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
if events&unix.FILE_ATTRIB != 0 && stat != nil {
|
|
// Only send Chmod if perms changed
|
|
if stat.Mode().Perm() != fmode.Perm() {
|
|
if !w.sendEvent(path, Chmod) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
if stat != nil {
|
|
// If we get here, it means we've hit an event above that requires us to
|
|
// continue watching the file or directory
|
|
return w.associateFile(path, stat, isWatched)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *Watcher) updateDirectory(path string) error {
|
|
// The directory was modified, so we must find unwatched entities and watch
|
|
// them. If something was removed from the directory, nothing will happen,
|
|
// as everything else should still be watched.
|
|
files, err := os.ReadDir(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, entry := range files {
|
|
path := filepath.Join(path, entry.Name())
|
|
if w.port.PathIsWatched(path) {
|
|
continue
|
|
}
|
|
|
|
finfo, err := entry.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = w.associateFile(path, finfo, false)
|
|
if err != nil {
|
|
if !w.sendError(err) {
|
|
return nil
|
|
}
|
|
}
|
|
if !w.sendEvent(path, Create) {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) error {
|
|
if w.isClosed() {
|
|
return ErrClosed
|
|
}
|
|
// This is primarily protecting the call to AssociatePath but it is
|
|
// important and intentional that the call to PathIsWatched is also
|
|
// protected by this mutex. Without this mutex, AssociatePath has been seen
|
|
// to error out that the path is already associated.
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
if w.port.PathIsWatched(path) {
|
|
// Remove the old association in favor of this one If we get ENOENT,
|
|
// then while the x/sys/unix wrapper still thought that this path was
|
|
// associated, the underlying event port did not. This call will have
|
|
// cleared up that discrepancy. The most likely cause is that the event
|
|
// has fired but we haven't processed it yet.
|
|
err := w.port.DissociatePath(path)
|
|
if err != nil && err != unix.ENOENT {
|
|
return err
|
|
}
|
|
}
|
|
// FILE_NOFOLLOW means we watch symlinks themselves rather than their
|
|
// targets.
|
|
events := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW
|
|
if follow {
|
|
// We *DO* follow symlinks for explicitly watched entries.
|
|
events = unix.FILE_MODIFIED | unix.FILE_ATTRIB
|
|
}
|
|
return w.port.AssociatePath(path, stat,
|
|
events,
|
|
stat.Mode())
|
|
}
|
|
|
|
func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool) error {
|
|
if !w.port.PathIsWatched(path) {
|
|
return nil
|
|
}
|
|
return w.port.DissociatePath(path)
|
|
}
|
|
|
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
|
// yet removed).
|
|
//
|
|
// Returns nil if [Watcher.Close] was called.
|
|
func (w *Watcher) WatchList() []string {
|
|
if w.isClosed() {
|
|
return nil
|
|
}
|
|
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
entries := make([]string, 0, len(w.watches)+len(w.dirs))
|
|
for pathname := range w.dirs {
|
|
entries = append(entries, pathname)
|
|
}
|
|
for pathname := range w.watches {
|
|
entries = append(entries, pathname)
|
|
}
|
|
|
|
return entries
|
|
}
|