Source File
backend_inotify.go
Belonging Package
github.com/fsnotify/fsnotify
//go:build linux && !appengine// +build linux,!appengine// Note: the documentation on the Watcher type and methods is generated from// mkdoc.zshpackage fsnotifyimport ()// 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.Errors chan error// Store fd here as os.File.Read() will no longer return on close after// calling Fd(). See: https://github.com/golang/go/issues/26439fd intinotifyFile *os.Filewatches *watchesdone chan struct{} // Channel for sending a "quit message" to the reader goroutinecloseMu sync.MutexdoneResp chan struct{} // Channel to respond to Close}type (watches struct {mu sync.RWMutexwd map[uint32]*watch // wd → watchpath map[string]uint32 // pathname → wd}watch struct {wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)path string // Watch path.})func newWatches() *watches {return &watches{wd: make(map[uint32]*watch),path: make(map[string]uint32),}}func ( *watches) () int {.mu.RLock()defer .mu.RUnlock()return len(.wd)}func ( *watches) ( *watch) {.mu.Lock()defer .mu.Unlock().wd[.wd] =.path[.path] = .wd}func ( *watches) ( uint32) {.mu.Lock()defer .mu.Unlock()delete(.path, .wd[].path)delete(.wd, )}func ( *watches) ( string) (uint32, bool) {.mu.Lock()defer .mu.Unlock(), := .path[]if ! {return 0, false}delete(.path, )delete(.wd, )return , true}func ( *watches) ( string) *watch {.mu.RLock()defer .mu.RUnlock()return .wd[.path[]]}func ( *watches) ( uint32) *watch {.mu.RLock()defer .mu.RUnlock()return .wd[]}func ( *watches) ( string, func(*watch) (*watch, error)) error {.mu.Lock()defer .mu.Unlock()var *watch, := .path[]if {= .wd[]}, := ()if != nil {return}if != nil {.wd[.wd] =.path[.path] = .wdif .wd != {delete(.wd, )}}return nil}// NewWatcher creates a new Watcher.func () (*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 ( uint) (*Watcher, error) {// Need to set nonblocking mode for SetDeadline to work, otherwise blocking// I/O operations won't terminate on close., := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)if == -1 {return nil,}:= &Watcher{fd: ,inotifyFile: os.NewFile(uintptr(), ""),watches: newWatches(),Events: make(chan Event, ),Errors: make(chan error),done: make(chan struct{}),doneResp: make(chan struct{}),}go .readEvents()return , nil}// Returns true if the event was sent, or false if watcher is closed.func ( *Watcher) ( Event) bool {select {case .Events <- :return truecase <-.done:return false}}// Returns true if the error was sent, or false if watcher is closed.func ( *Watcher) ( error) bool {select {case .Errors <- :return truecase <-.done:return false}}func ( *Watcher) () bool {select {case <-.done:return truedefault:return false}}// Close removes all watches and closes the Events channel.func ( *Watcher) () error {.closeMu.Lock()if .isClosed() {.closeMu.Unlock()return nil}close(.done).closeMu.Unlock()// Causes any blocking reads to return with an error, provided the file// still supports deadline operations.:= .inotifyFile.Close()if != nil {return}// Wait for goroutine to close<-.doneRespreturn nil}// 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 ( *Watcher) ( string) error { return .AddWith() }// 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 ( *Watcher) ( string, ...addOpt) error {if .isClosed() {return ErrClosed}= filepath.Clean()_ = getOptions(...)var uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELFreturn .watches.updatePath(, func( *watch) (*watch, error) {if != nil {|= .flags | unix.IN_MASK_ADD}, := unix.InotifyAddWatch(.fd, , )if == -1 {return nil,}if == nil {return &watch{wd: uint32(),path: ,flags: ,}, nil}.wd = uint32().flags =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 ( *Watcher) ( string) error {if .isClosed() {return nil}return .remove(filepath.Clean())}func ( *Watcher) ( string) error {, := .watches.removePath()if ! {return fmt.Errorf("%w: %s", ErrNonExistentWatch, )}, := unix.InotifyRmWatch(.fd, )if == -1 {// TODO: Perhaps it's not helpful to return an error here in every case;// The only two possible errors are://// - EBADF, which happens when w.fd is not a valid file descriptor// of any kind.// - EINVAL, which is when fd is not an inotify descriptor or wd// is not a valid watch descriptor. Watch descriptors are// invalidated when they are removed explicitly or implicitly;// explicitly by inotify_rm_watch, implicitly when the file they// are watching is deleted.return}return nil}// WatchList returns all paths explicitly added with [Watcher.Add] (and are not// yet removed).//// Returns nil if [Watcher.Close] was called.func ( *Watcher) () []string {if .isClosed() {return nil}:= make([]string, 0, .watches.len()).watches.mu.RLock()for := range .watches.path {= append(, )}.watches.mu.RUnlock()return}// readEvents reads from the inotify file descriptor, converts the// received events into Event objects and sends them via the Events channelfunc ( *Watcher) () {defer func() {close(.doneResp)close(.Errors)close(.Events)}()var ([unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw eventserror // Syscall errno)for {// See if we have been closed.if .isClosed() {return}, := .inotifyFile.Read([:])switch {case errors.Unwrap() == os.ErrClosed:returncase != nil:if !.sendError() {return}continue}if < unix.SizeofInotifyEvent {var errorif == 0 {= io.EOF // If EOF is received. This should really never happen.} else if < 0 {= // If an error occurred while reading.} else {= errors.New("notify: short read in readEvents()") // Read was too short.}if !.sendError() {return}continue}var uint32// We don't know how many events we just read into the buffer// While the offset points to at least one whole event...for <= uint32(-unix.SizeofInotifyEvent) {var (// Point "raw" to the event in the buffer= (*unix.InotifyEvent)(unsafe.Pointer(&[]))= uint32(.Mask)= uint32(.Len))if &unix.IN_Q_OVERFLOW != 0 {if !.sendError(ErrEventOverflow) {return}}// If the event happened to the watched directory or the watched file, the kernel// doesn't append the filename to the event, but we would like to always fill the// the "Name" field with a valid filename. We retrieve the path of the watch from// the "paths" map.:= .watches.byWd(uint32(.Wd))// inotify will automatically remove the watch on deletes; just need// to clean our state here.if != nil && &unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {.watches.remove(.wd)}// We can't really update the state when a watched path is moved;// only IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove// the watch.if != nil && &unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {:= .remove(.path)if != nil && !errors.Is(, ErrNonExistentWatch) {if !.sendError() {return}}}var stringif != nil {= .path}if > 0 {// Point "bytes" at the first byte of the filename:= (*[unix.PathMax]byte)(unsafe.Pointer(&[+unix.SizeofInotifyEvent]))[::]// The filename is padded with NULL bytes. TrimRight() gets rid of those.+= "/" + strings.TrimRight(string([0:]), "\000")}:= .newEvent(, )// Send the events that are not ignored on the events channelif &unix.IN_IGNORED == 0 {if !.sendEvent() {return}}// Move to the next event in the buffer+= unix.SizeofInotifyEvent +}}}// newEvent returns an platform-independent Event based on an inotify mask.func ( *Watcher) ( string, uint32) Event {:= Event{Name: }if &unix.IN_CREATE == unix.IN_CREATE || &unix.IN_MOVED_TO == unix.IN_MOVED_TO {.Op |= Create}if &unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || &unix.IN_DELETE == unix.IN_DELETE {.Op |= Remove}if &unix.IN_MODIFY == unix.IN_MODIFY {.Op |= Write}if &unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || &unix.IN_MOVED_FROM == unix.IN_MOVED_FROM {.Op |= Rename}if &unix.IN_ATTRIB == unix.IN_ATTRIB {.Op |= Chmod}return}
![]() |
The pages are generated with Golds v0.8.2. (GOOS=linux GOARCH=amd64) Golds is a Go 101 project developed by Tapir Liu. PR and bug reports are welcome and can be submitted to the issue list. Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds. |