Skip to content

Commit

Permalink
Add Solaris FEN using EventPorts from x/sys/unix
Browse files Browse the repository at this point in the history
  • Loading branch information
nshalman committed Aug 2, 2021
1 parent 5ce1ba4 commit 7b1c530
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 6 deletions.
3 changes: 3 additions & 0 deletions AUTHORS
Expand Up @@ -22,11 +22,14 @@ Daniel Wagner-Hall <dawagner@gmail.com>
Dave Cheney <dave@cheney.net>
Evan Phoenix <evan@fallingsnow.net>
Francisco Souza <f@souza.cc>
Gereon Frey <gereon.frey@gmail.com>
Hari haran <hariharan.uno@gmail.com>
Isaac Davis <isaac.davis@joyent.com>
John C Barstow
Kelvin Fo <vmirage@gmail.com>
Ken-ichirou MATSUZAWA <chamas@h4.dion.ne.jp>
Matt Layher <mdlayher@gmail.com>
Nahum Shalman <nahamu@gmail.com>
Nathan Youngman <git@nathany.com>
Nickolai Zeldovich <nickolai@csail.mit.edu>
Patrick <patrick@dropbox.com>
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog

## v1.5.0 / 2021-08-28

* Solaris: add support for File Event Notifications (fen) [#12](https://github.com/fsnotify/fsnotify/issues/12)

## v1.4.7 / 2018-01-09

* BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine)
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
@@ -1,5 +1,5 @@
Copyright (c) 2012 The Go Authors. All rights reserved.
Copyright (c) 2012-2019 fsnotify Authors. All rights reserved.
Copyright (c) 2012-2021 fsnotify Authors. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
Expand Down
259 changes: 257 additions & 2 deletions fen.go
Expand Up @@ -8,30 +8,285 @@ package fsnotify

import (
"errors"
"fmt"
"golang.org/x/sys/unix"
"io/ioutil"
"os"
"path/filepath"
)

// Watcher watches a set of files, delivering events to a channel.
type Watcher struct {
Events chan Event
Errors chan error

port *unix.EventPort

done chan struct{} // Channel for sending a "quit message" to the reader goroutine
doneResp chan struct{} // Channel to respond to Close
}

// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
func NewWatcher() (*Watcher, error) {
return nil, errors.New("FEN based watcher not yet supported for fsnotify\n")
var err error

w := new(Watcher)
w.Events = make(chan Event)
w.Errors = make(chan error)
w.port, err = unix.NewEventPort()
if err != nil {
return nil, err
}
w.done = make(chan struct{})
w.doneResp = make(chan struct{})

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(e Event) (sent bool) {
select {
case w.Events <- e:
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 {
if w.isClosed() {
return nil
}
close(w.done)
w.port.Close()
<-w.doneResp
return nil
}

// Add starts watching the named file or directory (non-recursively).
func (w *Watcher) Add(name string) error {
return nil
if w.isClosed() {
return errors.New("FEN watcher already closed")
}
if w.port.PathIsWatched(name) {
return nil
}
stat, err := os.Stat(name)
switch {
case err != nil:
return err
case stat.IsDir():
return w.handleDirectory(name, stat, w.associateFile)
default:
return w.associateFile(name, stat)
}
}

// Remove stops watching the the named file or directory (non-recursively).
func (w *Watcher) Remove(name string) error {
if w.isClosed() {
return errors.New("FEN watcher already closed")
}
if !w.port.PathIsWatched(name) {
return fmt.Errorf("can't remove non-existent FEN watch for: %s", name)
}
stat, err := os.Stat(name)
switch {
case err != nil:
return err
case stat.IsDir():
return w.handleDirectory(name, stat, w.dissociateFile)
default:
return w.port.DissociatePath(name)
}
}

// 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 close(w.doneResp)
defer close(w.Errors)
defer close(w.Events)

pevents := make([]unix.PortEvent, 8, 8)
for {
count, err := w.port.Get(pevents, 1, nil)
if err != nil {
// Interrupted system call (count should be 0) ignore and continue
if err == unix.EINTR && count == 0 {
continue
}
// Get failed because we called w.Close()
if 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, handler func(string, os.FileInfo) error) error {
files, err := ioutil.ReadDir(path)
if err != nil {
return err
}

// Handle all children of the directory.
for _, finfo := range files {
if !finfo.IsDir() {
err := handler(filepath.Join(path, finfo.Name()), finfo)
if err != nil {
return err
}
}
}

// And finally handle the directory itself.
return handler(path, stat)
}

func (w *Watcher) handleEvent(event *unix.PortEvent) error {
events := event.Events
path := event.Path
fmode := event.Cookie.(os.FileMode)

var toSend *Event
reRegister := true

switch {
case events&unix.FILE_MODIFIED == unix.FILE_MODIFIED:
if fmode.IsDir() {
if err := w.updateDirectory(path); err != nil {
return err
}
} else {
toSend = &Event{path, Write}
}
case events&unix.FILE_ATTRIB == unix.FILE_ATTRIB:
toSend = &Event{path, Chmod}
case events&unix.FILE_DELETE == unix.FILE_DELETE:
toSend = &Event{path, Remove}
reRegister = false
case events&unix.FILE_RENAME_FROM == unix.FILE_RENAME_FROM:
toSend = &Event{path, Rename}
// Don't keep watching the new file name
reRegister = false
case events&unix.FILE_RENAME_TO == unix.FILE_RENAME_TO:
// 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.
toSend = &Event{path, Remove}
// Don't keep watching the file that was removed
reRegister = false
default:
return errors.New("unknown event received")
}

if toSend != nil {
if !w.sendEvent(*toSend) {
return nil
}
}
if !reRegister {
return nil
}

// If we get here, it means we've hit an event above that requires us to
// continue watching the file or directory
stat, err := os.Stat(path)
if err != nil {
return err
}
return w.associateFile(path, stat)
}

func (w *Watcher) updateDirectory(path string) error {
// The directory was modified, so we must find unwatched entites and
// watch them. If something was removed from the directory, nothing will
// happen, as everything else should still be watched.
files, err := ioutil.ReadDir(path)
if err != nil {
return err
}

for _, finfo := range files {
path := filepath.Join(path, finfo.Name())
if w.port.PathIsWatched(path) {
continue
}

err := w.associateFile(path, finfo)
if err != nil {
if !w.sendError(err) {
return nil
}
}
if !w.sendEvent(Event{path, Create}) {
return nil
}
}
return nil
}

func (w *Watcher) associateFile(path string, stat os.FileInfo) error {
mode := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW
fmode := stat.Mode()
return w.port.AssociatePath(path, stat, mode, fmode)
}

func (w *Watcher) dissociateFile(path string, stat os.FileInfo) error {
if !w.port.PathIsWatched(path) {
return nil
}
return w.port.DissociatePath(path)
}
2 changes: 2 additions & 0 deletions go.mod
Expand Up @@ -3,3 +3,5 @@ module github.com/fsnotify/fsnotify
go 1.13

require golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9

replace golang.org/x/sys => github.com/nshalman/sys v0.0.0-20210702004434-76c4cd66abf6
4 changes: 2 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion integration_test.go
Expand Up @@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build !plan9,!solaris
// +build !plan9

package fsnotify

Expand Down

0 comments on commit 7b1c530

Please sign in to comment.