From 2a86751b60cb7f052f880e30b3c747735356ce2b Mon Sep 17 00:00:00 2001 From: lovezsh <1942314542@qq.com> Date: Tue, 26 May 2020 10:47:00 +0800 Subject: [PATCH] init --- README.md | 84 ++++++++++++++ example/main.go | 76 ++++++++++++ go.mod | 5 + go.sum | 2 + inotify.go | 300 ++++++++++++++++++++++++++++++++++++++++++++++++ poller.go | 73 ++++++++++++ poller_test.go | 85 ++++++++++++++ 7 files changed, 625 insertions(+) create mode 100644 README.md create mode 100644 example/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 inotify.go create mode 100644 poller.go create mode 100644 poller_test.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4ec2ba --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# 简介 + +Linux Inotify API的封装。 + +# 使用示例 + +```go +package main + +import ( + "fmt" + "inotify" + "log" + "os" + "path/filepath" + "time" +) + +func main() { + watcher, err := inotify.NewWatcher() + if err != nil { + log.Println(err) + } + name := os.Args[1] + paths := make([]string, 0) + if !isExistDir(name) { + paths = append(paths, name) + } else { + var err error + paths, err = dirs(name) + if err != nil { + log.Fatal(err) + } + } + for _, v := range paths { + err = watcher.AddWatch(v) + if err != nil { + log.Fatalf("failed to add watch %s, error: %v\n", os.Args[1], err) + } + } + + ticker := time.NewTicker(3 * time.Second) + for { + select { + case err := <-watcher.Errors: + log.Println(err) + os.Exit(1) + case event := <-watcher.Events: + if event.IsDir { + if event.Name == inotify.EventCreate { + err = watcher.AddWatch(event.Path) + if err != nil { + log.Fatalf("failed to add watch %s, error: %v\n", os.Args[1], err) + } + } + fmt.Println("Directory:", event.Path, "event:", event.Name) + } else { + fmt.Println("File:", event.Path, "event:", event.Name) + } + case <-ticker.C: + fmt.Println(watcher.Path()) + } + } +} + +func isExistDir(name string) bool { + f, err := os.Stat(name) + return err == nil && f.IsDir() +} + +func dirs(name string) ([]string, error) { + matchs := make([]string, 0) + err := filepath.Walk(name, func(root string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + matchs = append(matchs, root) + } + return nil + }) + return matchs, err +} +``` diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..6780a99 --- /dev/null +++ b/example/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "fmt" + "inotify" + "log" + "os" + "path/filepath" + "time" +) + +func main() { + watcher, err := inotify.NewWatcher() + if err != nil { + log.Println(err) + } + name := os.Args[1] + paths := make([]string, 0) + if !isExistDir(name) { + paths = append(paths, name) + } else { + var err error + paths, err = dirs(name) + if err != nil { + log.Fatal(err) + } + } + for _, v := range paths { + err = watcher.AddWatch(v) + if err != nil { + log.Fatalf("failed to add watch %s, error: %v\n", os.Args[1], err) + } + } + + ticker := time.NewTicker(3 * time.Second) + for { + select { + case err := <-watcher.Errors: + log.Println(err) + os.Exit(1) + case event := <-watcher.Events: + if event.IsDir { + if event.Name == inotify.EventCreate { + err = watcher.AddWatch(event.Path) + if err != nil { + log.Fatalf("failed to add watch %s, error: %v\n", os.Args[1], err) + } + } + fmt.Println("Directory:", event.Path, "event:", event.Name) + } else { + fmt.Println("File:", event.Path, "event:", event.Name) + } + case <-ticker.C: + fmt.Println(watcher.Path()) + } + } +} + +func isExistDir(name string) bool { + f, err := os.Stat(name) + return err == nil && f.IsDir() +} + +func dirs(name string) ([]string, error) { + matchs := make([]string, 0) + err := filepath.Walk(name, func(root string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + matchs = append(matchs, root) + } + return nil + }) + return matchs, err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c4a15fc --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module inotify + +go 1.13 + +require golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5486062 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/inotify.go b/inotify.go new file mode 100644 index 0000000..d51b502 --- /dev/null +++ b/inotify.go @@ -0,0 +1,300 @@ +package inotify + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +var ErrEventOverflow = fmt.Errorf("inotify event queue overflowed") + +type Event struct { + Name string + Path string + IsDir bool +} + +type Watcher struct { + Events chan Event + Errors chan error + fd int + poller *poller + pW map[int32]watch + wP map[string]int + wPLock *sync.Mutex + pWLock *sync.Mutex + done chan bool + isClosed bool +} + +func (w *Watcher) Path() []string { + paths := make([]string, 0) + + if w.isClosed { + return paths + } + + w.wPLock.Lock() + defer w.wPLock.Unlock() + + for path := range w.wP { + paths = append(paths, path) + } + + return paths +} + +func NewWatcher() (*Watcher, error) { + fd, err := unix.InotifyInit1(unix.IN_NONBLOCK) + if err != nil { + return nil, err + } + poller, err := newPoller(fd) + if err != nil { + return nil, err + } + w := &Watcher{ + fd: fd, + poller: poller, + Events: make(chan Event), + Errors: make(chan error), + pW: make(map[int32]watch), + pWLock: new(sync.Mutex), + wP: make(map[string]int), + wPLock: new(sync.Mutex), + done: make(chan bool), + } + go w.eventLoop() + return w, nil +} + +const bufsize = unix.SizeofInotifyEvent + syscall.NAME_MAX + 1 + +type eventBuf [bufsize]byte + +func (w *Watcher) readEvent(buf eventBuf, n int) { + var offset uint32 + for offset <= uint32(n-unix.SizeofInotifyEvent) { + event := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) + if event.Mask&unix.IN_Q_OVERFLOW == unix.IN_Q_OVERFLOW { + w.Errors <- ErrEventOverflow + break + } + + var name string + w.pWLock.Lock() + path := w.pW[event.Wd] + w.pWLock.Unlock() + + if event.Len > 0 { + rawName := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:event.Len:event.Len] + name = strings.TrimRight(string(rawName[0:event.Len]), "\000") + } + + var isDir bool + if isDeleteSelf(event.Mask) { + isDir = path.isDir + w.remove(event.Wd) + } else { + isDir = isDirectory(event.Mask) + } + + if !isIgnore(event.Mask) { + w.Events <- Event{ + Name: maskString(event.Mask), + Path: filepath.Join(path.name, name), + IsDir: isDir, + } + } + + offset += event.Len + unix.SizeofInotifyEvent + } +} + +func (w *Watcher) Close() { + if w.isClosed { + return + } + w.poller.close() + w.done <- true + close(w.Errors) + close(w.Events) + close(w.done) +} + +func (w *Watcher) readEventLoop() { + var ( + n int + err error + buf eventBuf + ) + + for { + select { + case <-w.done: + return + default: + } + + n, err = unix.Read(w.fd, buf[:]) + if err != nil { + if err == unix.EINTR { + break + } + if err == unix.EAGAIN { + break + } + w.Errors <- err + continue + } + + if n < unix.SizeofInotifyEvent { + w.Errors <- fmt.Errorf("event is too short") + break + } + + w.readEvent(buf, n) + } + +} + +func (w *Watcher) eventLoop() { + var ( + ok bool + err error + ) + + for { + ok, err = w.poller.wait() + if err != nil { + w.Errors <- err + continue + } + if !ok { + continue + } + w.readEventLoop() + } +} + +func (w *Watcher) remove(wd int32) { + w.pWLock.Lock() + defer w.pWLock.Unlock() + name, ok := w.pW[wd] + if !ok { + return + } + delete(w.pW, wd) + + w.wPLock.Lock() + defer w.wPLock.Unlock() + delete(w.wP, name.name) +} + +type watch struct { + name string + isDir bool +} + +func (w *Watcher) AddWatch(name string) error { + name = filepath.Clean(name) + wd, err := unix.InotifyAddWatch(w.fd, name, unix.IN_ALL_EVENTS) + if err != nil { + return err + } + + w.pWLock.Lock() + w.pW[int32(wd)] = watch{name: name, isDir: !isExistFile(name)} + w.pWLock.Unlock() + + w.wPLock.Lock() + w.wP[name] = wd + w.wPLock.Unlock() + + return nil +} + +func (w *Watcher) RemoveWatch(name string) error { + name = filepath.Clean(name) + w.wPLock.Lock() + defer w.wPLock.Unlock() + wd, ok := w.wP[name] + if !ok { + return fmt.Errorf("can't remove non-existent inotify watch for: %s", name) + } + delete(w.wP, name) + + w.pWLock.Lock() + defer w.pWLock.Unlock() + delete(w.pW, int32(wd)) + + _, err := unix.InotifyRmWatch(w.fd, uint32(wd)) + return err +} + +func maskString(mask uint32) string { + for key, value := range eventName { + if key&mask != 0 { + return value + } + } + return EventUnknown +} + +const ( + EventAccess = "IN_ACCESS" + EventAttrib = "IN_ATTRIB" + EventCloseWrite = "IN_CLOSE_WRITE" + EventCloseNoWrite = "IN_CLOSE_NOWRITE" + EventCreate = "IN_CREATE" + EventDelete = "IN_DELETE" + EventDeleteSelf = "IN_DELETE_SELF" + EventModify = "IN_MODIFY" + EventMoveSelf = "IN_MOVE_SELF" + EventMovedFrom = "IN_MOVED_FROM" + EventMovedTo = "IN_MOVED_TO" + EventOpen = "IN_OPEN" + EventIgnored = "IN_IGNORED" + EventUnmount = "IN_UNMOUNT" + EventUnknown = "UNKNOWN" +) + +var eventName = map[uint32]string{ + unix.IN_ACCESS: EventAccess, + unix.IN_ATTRIB: EventAttrib, + unix.IN_CLOSE_WRITE: EventCloseWrite, + unix.IN_CLOSE_NOWRITE: EventCloseNoWrite, + unix.IN_CREATE: EventCreate, + unix.IN_DELETE: EventDelete, + unix.IN_DELETE_SELF: EventDeleteSelf, + unix.IN_MODIFY: EventModify, + unix.IN_MOVE_SELF: EventMoveSelf, + unix.IN_MOVED_FROM: EventMovedFrom, + unix.IN_MOVED_TO: EventMovedTo, + unix.IN_OPEN: EventOpen, + unix.IN_IGNORED: EventIgnored, + unix.IN_UNMOUNT: EventUnmount, +} + +func isDirectory(mask uint32) bool { + return mask&unix.IN_ISDIR == unix.IN_ISDIR +} + +func isIgnore(mask uint32) bool { + return mask&unix.IN_IGNORED == unix.IN_IGNORED +} + +func isDeleteSelf(mask uint32) bool { + return mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF +} + +func isExistFile(name string) bool { + f, err := os.Stat(name) + return err == nil && !f.IsDir() +} diff --git a/poller.go b/poller.go new file mode 100644 index 0000000..f1d03f3 --- /dev/null +++ b/poller.go @@ -0,0 +1,73 @@ +package inotify + +import ( + "errors" + + "golang.org/x/sys/unix" +) + +type poller struct { + fd int + epfd int +} + +func newPoller(fd int) (*poller, error) { + var err error + + p := &poller{fd: -1, epfd: -1} + p.fd = fd + p.epfd, err = unix.EpollCreate1(unix.EPOLL_CLOEXEC) + if err != nil { + return nil, err + } + + event := unix.EpollEvent{ + Fd: int32(p.fd), + Events: unix.EPOLLIN | unix.EPOLLET | unix.EPOLLERR | unix.EPOLLHUP, + } + err = unix.EpollCtl(p.epfd, unix.EPOLL_CTL_ADD, p.fd, &event) + if err != nil { + return nil, err + } + + return p, nil +} + +func (p *poller) wait() (bool, error) { + for { + events := make([]unix.EpollEvent, 1) + + n, err := unix.EpollWait(p.epfd, events, -1) + if err != nil { + if err == unix.EINTR { + continue + } + return false, err + } + + if n == 0 { + continue + } + + event := events[0] + if event.Events&unix.EPOLLHUP != 0 { + return false, errors.New("epoll hup") + } + if event.Events&unix.EPOLLERR != 0 { + return false, errors.New("epoll error") + } + if event.Events&unix.EPOLLIN != 0 { + return true, nil + } + return false, errors.New("unkown epoll event") + } +} + +func (p *poller) close() { + if p.fd != -1 { + unix.Close(p.fd) + } + if p.epfd != -1 { + unix.Close(p.epfd) + } +} diff --git a/poller_test.go b/poller_test.go new file mode 100644 index 0000000..4d3cfd2 --- /dev/null +++ b/poller_test.go @@ -0,0 +1,85 @@ +package inotify + +import ( + "testing" + "time" + + "golang.org/x/sys/unix" +) + +func TestPollerWithBadFd(t *testing.T) { + _, err := newPoller(-1) + if err != unix.EBADF { + t.Fatalf("Expected EBADF, got: %v", err) + } +} + +func TestPollerWithData(t *testing.T) { + var tfd [2]int + err := unix.Pipe(tfd[:]) + if err != nil { + t.Fatalf("failed to create pipe: %v", err) + } + defer func() { + unix.Close(tfd[0]) + unix.Close(tfd[1]) + }() + poller, err := newPoller(tfd[0]) + if err != nil { + t.Fatalf("Failed to create poller: %v", err) + } + oks := make(chan bool) + go func() { + ok, err := poller.wait() + if err != nil { + t.Fatalf("poller failed: %v", err) + } + oks <- ok + }() + + select { + case <-time.After(1000 * time.Millisecond): + case <-oks: + t.Fatalf("poller did not wait") + } + + msg := "poller" + buf := []byte(msg) + _, err = unix.Write(tfd[1], buf) + if err != nil { + t.Fatalf("Failed to write to pipe: %v", err) + } + + ok := <-oks + if !ok { + t.Fatalf("expected true") + } + buf2 := make([]byte, 2048) + n, err := unix.Read(tfd[0], buf2) + if string(buf2[:n]) != msg { + t.Fatalf("read data error") + } +} + +func TestPollerWithClose(t *testing.T) { + var tfd [2]int + err := unix.Pipe(tfd[:]) + if err != nil { + t.Fatalf("failed to create pipe: %v", err) + } + defer func() { + unix.Close(tfd[0]) + }() + poller, err := newPoller(tfd[0]) + if err != nil { + t.Fatalf("Failed to create poller: %v", err) + } + unix.Close(tfd[1]) + ok, err := poller.wait() + if err == nil { + t.Fatalf("except poller") + } + if ok { + t.Fatalf("expected poller to return true") + } +}