notify

package module
v0.0.0-...-c6b7342 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 16, 2021 License: MIT Imports: 12 Imported by: 5

README

notify GoDoc Build Status Build status Coverage Status

Filesystem event notification library on steroids. (under active development)

Documentation

godoc.org/github.com/rjeczalik/notify

Installation

~ $ go get -u github.com/rjeczalik/notify

Projects using notify

Documentation

Overview

Package notify implements access to filesystem events.

Notify is a high-level abstraction over filesystem watchers like inotify, kqueue, FSEvents, FEN or ReadDirectoryChangesW. Watcher implementations are split into two groups: ones that natively support recursive notifications (FSEvents and ReadDirectoryChangesW) and ones that do not (inotify, kqueue, FEN). For more details see watcher and recursiveWatcher interfaces in watcher.go source file.

On top of filesystem watchers notify maintains a watchpoint tree, which provides a strategy for creating and closing filesystem watches and dispatching filesystem events to user channels.

An event set is just an event list joint using bitwise OR operator into a single event value. Both the platform-independent (see Constants) and specific events can be used. Refer to the event_*.go source files for information about the available events.

A filesystem watch or just a watch is platform-specific entity which represents a single path registered for notifications for specific event set. Setting a watch means using platform-specific API calls for creating / initializing said watch. For each watcher the API call is:

  • FSEvents: FSEventStreamCreate
  • inotify: notify_add_watch
  • kqueue: kevent
  • ReadDirectoryChangesW: CreateFile+ReadDirectoryChangesW
  • FEN: port_get

To rewatch means to either shrink or expand an event set that was previously registered during watch operation for particular filesystem watch.

A watchpoint is a list of user channel and event set pairs for particular path (watchpoint tree's node). A single watchpoint can contain multiple different user channels registered to listen for one or more events. A single user channel can be registered in one or more watchpoints, recursive and non-recursive ones as well.

Index

Examples

Constants

View Source
const (
	Create = osSpecificCreate
	Remove = osSpecificRemove
	Write  = osSpecificWrite
	Rename = osSpecificRename

	// All is handful alias for all platform-independent event values.
	All = Create | Remove | Write | Rename
)

Create, Remove, Write and Rename are the only event values guaranteed to be present on all platforms.

View Source
const (
	InAccess       = Event(unix.IN_ACCESS)        // File was accessed
	InModify       = Event(unix.IN_MODIFY)        // File was modified
	InAttrib       = Event(unix.IN_ATTRIB)        // Metadata changed
	InCloseWrite   = Event(unix.IN_CLOSE_WRITE)   // Writtable file was closed
	InCloseNowrite = Event(unix.IN_CLOSE_NOWRITE) // Unwrittable file closed
	InOpen         = Event(unix.IN_OPEN)          // File was opened
	InMovedFrom    = Event(unix.IN_MOVED_FROM)    // File was moved from X
	InMovedTo      = Event(unix.IN_MOVED_TO)      // File was moved to Y
	InCreate       = Event(unix.IN_CREATE)        // Subfile was created
	InDelete       = Event(unix.IN_DELETE)        // Subfile was deleted
	InDeleteSelf   = Event(unix.IN_DELETE_SELF)   // Self was deleted
	InMoveSelf     = Event(unix.IN_MOVE_SELF)     // Self was moved
)

Inotify specific masks are legal, implemented events that are guaranteed to work with notify package on linux-based systems.

Variables

This section is empty.

Functions

func Stop

func Stop(c chan<- EventInfo)

Stop removes all watchpoints registered for c. All underlying watches are also removed, for which c was the last channel listening for events.

Stop does not close c. When Stop returns, it is guaranteed that c will receive no more signals.

Example

This example shows why it is important to not create leaks by stoping a channel when it's no longer being used.

package main

import (
	"log"
	"path/filepath"
	"time"

	"github.com/syncthing/notify"
)

func main() {
	waitfor := func(path string, e notify.Event, timeout time.Duration) bool {
		dir, file := filepath.Split(path)
		c := make(chan notify.EventInfo, 1)

		if err := notify.Watch(dir, c, e); err != nil {
			log.Fatal(err)
		}
		// Clean up watchpoint associated with c. If Stop was not called upon
		// return the channel would be leaked as notify holds the only reference
		// to it and does not release it on its own.
		defer notify.Stop(c)

		t := time.After(timeout)

		for {
			select {
			case ei := <-c:
				if filepath.Base(ei.Path()) == file {
					return true
				}
			case <-t:
				return false
			}
		}
	}

	if waitfor("index.lock", notify.Create, 5*time.Second) {
		log.Println("The git repository was locked")
	}

	if waitfor("index.lock", notify.Remove, 5*time.Second) {
		log.Println("The git repository was unlocked")
	}
}
Output:

func Watch

func Watch(path string, c chan<- EventInfo, events ...Event) error

Watch sets up a watchpoint on path listening for events given by the events argument.

File or directory given by the path must exist, otherwise Watch will fail with non-nil error. Notify resolves, for its internal purpose, any symlinks the provided path may contain, so it may fail if the symlinks form a cycle. It does so, since not all watcher implementations treat passed paths as-is. E.g. FSEvents reports a real path for every event, setting a watchpoint on /tmp will report events with paths rooted at /private/tmp etc.

The c almost always is a buffered channel. Watch will not block sending to c - the caller must ensure that c has sufficient buffer space to keep up with the expected event rate.

It is allowed to pass the same channel multiple times with different event list or different paths. Calling Watch with different event lists for a single watchpoint expands its event set. The only way to shrink it, is to call Stop on its channel.

Calling Watch with empty event list does not expand nor shrink watchpoint's event set. If c is the first channel to listen for events on the given path, Watch will seamlessly create a watch on the filesystem.

Notify dispatches copies of single filesystem event to all channels registered for each path. If a single filesystem event contains multiple coalesced events, each of them is dispatched separately. E.g. the following filesystem change:

~ $ echo Hello > Notify.txt

dispatches two events - notify.Create and notify.Write. However, it may depend on the underlying watcher implementation whether OS reports both of them.

Windows and recursive watches

If a directory which path was used to create recursive watch under Windows gets deleted, the OS will not report such event. It is advised to keep in mind this limitation while setting recursive watchpoints for your application, e.g. use persistent paths like %userprofile% or watch additionally parent directory of a recursive watchpoint in order to receive delete events for it.

Example

This is a basic example showing how to work with notify.Watch function.

package main

import (
	"log"

	"github.com/syncthing/notify"
)

func main() {
	// Make the channel buffered to ensure no event is dropped. Notify will drop
	// an event if the receiver is not able to keep up the sending pace.
	c := make(chan notify.EventInfo, 1)

	// Set up a watchpoint listening on events within current working directory.
	// Dispatch each create and remove events separately to c.
	if err := notify.Watch(".", c, notify.Create, notify.Remove); err != nil {
		log.Fatal(err)
	}
	defer notify.Stop(c)

	// Block until an event is received.
	ei := <-c
	log.Println("Got event:", ei)
}
Output:

Example (Linux)

This example shows how to watch changes made on file-system by text editor when saving a file. Usually, either InCloseWrite or InMovedTo (when swapping with a temporary file) event is created.

package main

import (
	"log"

	"github.com/syncthing/notify"
)

func main() {
	// Make the channel buffered to ensure no event is dropped. Notify will drop
	// an event if the receiver is not able to keep up the sending pace.
	c := make(chan notify.EventInfo, 1)

	// Set up a watchpoint listening for inotify-specific events within a
	// current working directory. Dispatch each InCloseWrite and InMovedTo
	// events separately to c.
	if err := notify.Watch(".", c, notify.InCloseWrite, notify.InMovedTo); err != nil {
		log.Fatal(err)
	}
	defer notify.Stop(c)

	// Block until an event is received.
	switch ei := <-c; ei.Event() {
	case notify.InCloseWrite:
		log.Println("Editing of", ei.Path(), "file is done.")
	case notify.InMovedTo:
		log.Println("File", ei.Path(), "was swapped/moved into the watched directory.")
	}
}
Output:

Example (LinuxMove)

This example shows how to use Sys() method from EventInfo interface to tie two separate events generated by rename(2) function.

package main

import (
	"log"

	"golang.org/x/sys/unix"

	"github.com/syncthing/notify"
)

func main() {
	// Make the channel buffered to ensure no event is dropped. Notify will drop
	// an event if the receiver is not able to keep up the sending pace.
	c := make(chan notify.EventInfo, 2)

	// Set up a watchpoint listening for inotify-specific events within a
	// current working directory. Dispatch each InMovedFrom and InMovedTo
	// events separately to c.
	if err := notify.Watch(".", c, notify.InMovedFrom, notify.InMovedTo); err != nil {
		log.Fatal(err)
	}
	defer notify.Stop(c)

	// Inotify reports move filesystem action by sending two events tied with
	// unique cookie value (uint32): one of the events is of InMovedFrom type
	// carrying move source path, while the second one is of InMoveTo type
	// carrying move destination path.
	moves := make(map[uint32]struct {
		From string
		To   string
	})

	// Wait for moves.
	for ei := range c {
		cookie := ei.Sys().(*unix.InotifyEvent).Cookie

		info := moves[cookie]
		switch ei.Event() {
		case notify.InMovedFrom:
			info.From = ei.Path()
		case notify.InMovedTo:
			info.To = ei.Path()
		}
		moves[cookie] = info

		if cookie != 0 && info.From != "" && info.To != "" {
			log.Println("File:", info.From, "was renamed to", info.To)
			delete(moves, cookie)
		}
	}
}
Output:

Example (Recursive)

This example shows how to set up a recursive watchpoint.

package main

import (
	"log"

	"github.com/syncthing/notify"
)

func main() {
	// Make the channel buffered to ensure no event is dropped. Notify will drop
	// an event if the receiver is not able to keep up the sending pace.
	c := make(chan notify.EventInfo, 1)

	// Set up a watchpoint listening for events within a directory tree rooted
	// at current working directory. Dispatch remove events to c.
	if err := notify.Watch("./...", c, notify.Remove); err != nil {
		log.Fatal(err)
	}
	defer notify.Stop(c)

	// Block until an event is received.
	ei := <-c
	log.Println("Got event:", ei)
}
Output:

func WatchWithFilter

func WatchWithFilter(path string, c chan<- EventInfo,
	doNotWatch func(string) bool, events ...Event) error

This function works the same way as Watch. In addition it does not watch files or directories based on the return value of the argument function doNotWatch. Given a path as argument doNotWatch should return true if the file or directory should not be watched.

Types

type DoNotWatchFn

type DoNotWatchFn func(string) bool

type Event

type Event uint32

Event represents the type of filesystem action.

Number of available event values is dependent on the target system or the watcher implmenetation used (e.g. it's possible to use either kqueue or FSEvents on Darwin).

Please consult documentation for your target platform to see list of all available events.

func (Event) String

func (e Event) String() string

String implements fmt.Stringer interface.

type EventInfo

type EventInfo interface {
	Event() Event     // event value for the filesystem action
	Path() string     // real path of the file or directory
	Sys() interface{} // underlying data source (can return nil)
}

EventInfo describes an event reported by the underlying filesystem notification subsystem.

It always describes single event, even if the OS reported a coalesced action. Reported path is absolute and clean.

For non-recursive watchpoints its base is always equal to the path passed to corresponding Watch call.

The value of Sys if system-dependent and can be nil.

Sys

Under Darwin (FSEvents) Sys() always returns a non-nil *notify.FSEvent value, which is defined as:

type FSEvent struct {
    Path  string // real path of the file or directory
    ID    uint64 // ID of the event (FSEventStreamEventId)
    Flags uint32 // joint FSEvents* flags (FSEventStreamEventFlags)
}

For possible values of Flags see Darwin godoc for notify or FSEvents documentation for FSEventStreamEventFlags constants:

https://developer.apple.com/library/mac/documentation/Darwin/Reference/FSEvents_Ref/index.html#//apple_ref/doc/constant_group/FSEventStreamEventFlags

Under Linux (inotify) Sys() always returns a non-nil *unix.InotifyEvent value, defined as:

type InotifyEvent struct {
    Wd     int32    // Watch descriptor
    Mask   uint32   // Mask describing event
    Cookie uint32   // Unique cookie associating related events (for rename(2))
    Len    uint32   // Size of name field
    Name   [0]uint8 // Optional null-terminated name
}

More information about inotify masks and the usage of inotify_event structure can be found at:

http://man7.org/linux/man-pages/man7/inotify.7.html

Under Darwin, DragonFlyBSD, FreeBSD, NetBSD, OpenBSD (kqueue) Sys() always returns a non-nil *notify.Kevent value, which is defined as:

type Kevent struct {
    Kevent *syscall.Kevent_t // Kevent is a kqueue specific structure
    FI     os.FileInfo       // FI describes file/dir
}

More information about syscall.Kevent_t can be found at:

https://www.freebsd.org/cgi/man.cgi?query=kqueue

Under Windows (ReadDirectoryChangesW) Sys() always returns nil. The documentation of watcher's WinAPI function can be found at:

https://msdn.microsoft.com/en-us/library/windows/desktop/aa365465%28v=vs.85%29.aspx

Notes

Bugs

  • Notify does not collect watchpoints, when underlying watches were removed by their os-specific watcher implementations. Instead users are advised to listen on persistent paths to have guarantee they receive events for the whole lifetime of their applications (to discuss see #69).

  • Linux (inotify) does not support watcher behavior masks like InOneshot, InOnlydir etc. Instead users are advised to perform the filtering themselves (to discuss see #71).

  • Notify was not tested for short path name support under Windows (ReadDirectoryChangesW).

  • Windows (ReadDirectoryChangesW) cannot recognize which notification triggers FileActionModified event. (to discuss see #75).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL