Skip to content

Commit

Permalink
Replaced use of raw epoll with netpoller read
Browse files Browse the repository at this point in the history
  • Loading branch information
horahoradev committed Mar 27, 2022
1 parent 712fe1d commit 520c763
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 483 deletions.
56 changes: 56 additions & 0 deletions .github/workflows/vagrant_test.yml
@@ -0,0 +1,56 @@
# MIT License

# Copyright (c) 2021 Jonas Hecht

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# https://github.com/jonashackt/vagrant-github-actions/

name: vagrant_test
on:
push:
pull_request:
jobs:
test:
strategy:
fail-fast: false
matrix:
vagrant_image:
- debian6
runs-on: macos-10.15
steps:
- uses: actions/checkout@v2

- name: setup Go
uses: actions/setup-go@v2
with:
go-version: '1.17'

- name: Show Vagrant version
run: vagrant --version

- name: compile test binary
run: |
GOOS=linux GOARCH=amd64 go test -o Vagrantfiles/${{ matrix.vagrant_image}}/fsnotify.test -c ./...
- name: Run vagrant up
run: cd Vagrantfiles/${{ matrix.vagrant_image}} && vagrant up

- name: run the test binary
run: cd Vagrantfiles/${{ matrix.vagrant_image}} && vagrant ssh -c "/vagrant/fsnotify.test"
7 changes: 7 additions & 0 deletions Vagrantfiles/debian6/Vagrantfile
@@ -0,0 +1,7 @@
Vagrant.configure("2") do |config|
config.vm.box = "threatstack/debian6"
config.vm.box_version = "1.0.0"

config.vm.define 'debian6'

end
98 changes: 36 additions & 62 deletions inotify.go
Expand Up @@ -22,15 +22,15 @@ import (

// Watcher watches a set of files, delivering events to a channel.
type Watcher struct {
Events chan Event
Errors chan error
mu sync.Mutex // Map access
fd int
poller *fdPoller
watches map[string]*watch // Map of inotify watches (key: path)
paths map[int]string // Map of watched paths (key: watch descriptor)
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
doneResp chan struct{} // Channel to respond to Close
fd int // https://github.com/golang/go/issues/26439 can't call .Fd() on os.FIle or Read will no longer return on Close()
Events chan Event
Errors chan error
mu sync.Mutex // Map access
inotifyFile *os.File
watches map[string]*watch // Map of inotify watches (key: path)
paths map[int]string // Map of watched paths (key: watch descriptor)
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.
Expand All @@ -40,21 +40,23 @@ func NewWatcher() (*Watcher, error) {
if fd == -1 {
return nil, errno
}
// Create epoll
poller, err := newFdPoller(fd)
if err != nil {
unix.Close(fd)
return nil, err

// Need to set the FD to nonblocking mode in order for SetDeadline methods to work
// Otherwise, blocking i/o operations won't terminate on close
errno = unix.SetNonblock(fd, true)
if errno != nil {
return nil, errno
}

w := &Watcher{
fd: fd,
poller: poller,
watches: make(map[string]*watch),
paths: make(map[int]string),
Events: make(chan Event),
Errors: make(chan error),
done: make(chan struct{}),
doneResp: make(chan struct{}),
fd: fd,
inotifyFile: os.NewFile(uintptr(fd), ""),
watches: make(map[string]*watch),
paths: make(map[int]string),
Events: make(chan Event),
Errors: make(chan error),
done: make(chan struct{}),
doneResp: make(chan struct{}),
}

go w.readEvents()
Expand All @@ -79,8 +81,11 @@ func (w *Watcher) Close() error {
// Send 'close' signal to goroutine, and set the Watcher to closed.
close(w.done)

// Wake up goroutine
w.poller.wake()
// Causes any blocking reads to return with an error, provided the file still supports deadline operations
err := w.inotifyFile.Close()
if err != nil {
return err
}

// Wait for goroutine to close
<-w.doneResp
Expand Down Expand Up @@ -173,50 +178,32 @@ type watch struct {
func (w *Watcher) readEvents() {
var (
buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
n int // Number of bytes read with read()
errno error // Syscall errno
ok bool // For poller.wait
)

defer close(w.doneResp)
defer close(w.Errors)
defer close(w.Events)
defer unix.Close(w.fd)
defer w.poller.close()

for {
// See if we have been closed.
if w.isClosed() {
return
}

ok, errno = w.poller.wait()
if errno != nil {
n, err := w.inotifyFile.Read(buf[:])
switch {
case errors.Unwrap(err) == os.ErrClosed:
return
case err != nil:
select {
case w.Errors <- errno:
case w.Errors <- err:
case <-w.done:
return
}
continue
}

if !ok {
continue
}

n, errno = unix.Read(w.fd, buf[:])
// If a signal interrupted execution, see if we've been asked to close, and try again.
// http://man7.org/linux/man-pages/man7/signal.7.html :
// "Before Linux 3.8, reads from an inotify(7) file descriptor were not restartable"
if errno == unix.EINTR {
continue
}

// unix.Read might have been woken up by Close. If so, we're done.
if w.isClosed() {
return
}

if n < unix.SizeofInotifyEvent {
var err error
if n == 0 {
Expand Down Expand Up @@ -300,20 +287,7 @@ func (w *Watcher) readEvents() {
// against files that do not exist.
func (e *Event) ignoreLinux(mask uint32) bool {
// Ignore anything the inotify API says to ignore
if mask&unix.IN_IGNORED == unix.IN_IGNORED {
return true
}

// If the event is not a DELETE or RENAME, the file must exist.
// Otherwise the event is ignored.
// *Note*: this was put in place because it was seen that a MODIFY
// event was sent after the DELETE. This ignores that MODIFY and
// assumes a DELETE will come or has come if the file doesn't exist.
if !(e.Op&Remove == Remove || e.Op&Rename == Rename) {
_, statErr := os.Lstat(e.Name)
return os.IsNotExist(statErr)
}
return false
return mask&unix.IN_IGNORED == unix.IN_IGNORED
}

// newEvent returns an platform-independent Event based on an inotify mask.
Expand Down

0 comments on commit 520c763

Please sign in to comment.