diff --git a/cmd/pspy/main.go b/cmd/pspy/main.go index aadd16b..d76635c 100644 --- a/cmd/pspy/main.go +++ b/cmd/pspy/main.go @@ -1,7 +1,7 @@ package main -import "github.com/dominicbreuker/pspy/internal/dev" +import "github.com/dominicbreuker/pspy/internal/cmd" func main() { - dev.Monitor() + cmd.Monitor() } diff --git a/internal/dev/plist.go b/internal/cmd/pspy.go similarity index 59% rename from internal/dev/plist.go rename to internal/cmd/pspy.go index 71e5c5e..025effa 100644 --- a/internal/dev/plist.go +++ b/internal/cmd/pspy.go @@ -1,4 +1,4 @@ -package dev +package cmd import ( "log" @@ -9,26 +9,18 @@ import ( "github.com/dominicbreuker/pspy/internal/walker" ) -type Process struct { - pid int - ppid int - state rune - pgrp int - sid int - - binary string -} +const MaxInt = int(^uint(0) >> 1) func Monitor() { - watch() + watch([]string{"/tmp"}, nil) } -func watch() { +func watch(rdirs, dirs []string) { maxWatchers, err := inotify.WatcherLimit() if err != nil { log.Printf("Can't get inotify watcher limit...: %v\n", err) } - log.Printf("Watcher limit: %d\n", maxWatchers) + log.Printf("Inotify watcher limit: %d (/proc/sys/fs/inotify/max_user_watches)\n", maxWatchers) ping := make(chan struct{}) in, err := inotify.NewInotify(ping) @@ -37,9 +29,37 @@ func watch() { } defer in.Close() - dirCh, errCh := walker.Walk("/tmp") + for _, dir := range rdirs { + addWatchers(dir, MaxInt, in, maxWatchers) + } + for _, dir := range dirs { + addWatchers(dir, 0, in, maxWatchers) + } + + log.Printf("Inotify watchers set up: %s\n", in) + + procList := process.NewProcList() + + ticker := time.NewTicker(100 * time.Millisecond) + + for { + select { + case <-ticker.C: + refresh(in, procList) + case <-ping: + refresh(in, procList) + } + } +} + +func addWatchers(dir string, depth int, in *inotify.Inotify, maxWatchers int) { + dirCh, errCh, doneCh := walker.Walk(dir, depth) loop: for { + if in.NumWatchers() >= maxWatchers { + close(doneCh) + break loop + } select { case dir, ok := <-dirCh: if !ok { @@ -52,21 +72,6 @@ loop: log.Printf("Error walking filesystem: %v", err) } } - - log.Printf("Inotify set up: %s\n", in) - - procList := process.NewProcList() - - ticker := time.NewTicker(100 * time.Millisecond).C - - for { - select { - case <-ticker: - refresh(in, procList) - case <-ping: - refresh(in, procList) - } - } } func refresh(in *inotify.Inotify, pl *process.ProcList) { @@ -74,6 +79,6 @@ func refresh(in *inotify.Inotify, pl *process.ProcList) { if err := pl.Refresh(); err != nil { log.Printf("ERROR refreshing process list: %v", err) } - time.Sleep(10 * time.Millisecond) + time.Sleep(5 * time.Millisecond) in.UnPause() } diff --git a/internal/inotify/event.go b/internal/inotify/event.go new file mode 100644 index 0000000..ee7d252 --- /dev/null +++ b/internal/inotify/event.go @@ -0,0 +1,88 @@ +package inotify + +import ( + "bytes" + "fmt" + "log" + "strconv" + "unsafe" + + "golang.org/x/sys/unix" +) + +var InotifyEvents = map[uint32]string{ + unix.IN_ACCESS: "ACCESS", + unix.IN_ATTRIB: "ATTRIB", + unix.IN_CLOSE_NOWRITE: "CLOSE_NOWRITE", + unix.IN_CLOSE_WRITE: "CLOSE_WRITE", + unix.IN_CREATE: "CREATE", + unix.IN_DELETE: "DELETE", + unix.IN_DELETE_SELF: "DELETE_SELF", + unix.IN_MODIFY: "MODIFY", + unix.IN_MOVED_FROM: "MOVED_FROM", + unix.IN_MOVED_TO: "MOVED_TO", + unix.IN_MOVE_SELF: "MOVE_SELF", + unix.IN_OPEN: "OPEN", + (unix.IN_ACCESS | unix.IN_ISDIR): "ACCESS DIR", + (unix.IN_ATTRIB | unix.IN_ISDIR): "ATTRIB DIR", + (unix.IN_CLOSE_NOWRITE | unix.IN_ISDIR): "CLOSE_NOWRITE DIR", + (unix.IN_CLOSE_WRITE | unix.IN_ISDIR): "CLOSE_WRITE DIR", + (unix.IN_CREATE | unix.IN_ISDIR): "CREATE DIR", + (unix.IN_DELETE | unix.IN_ISDIR): "DELETE DIR", + (unix.IN_DELETE_SELF | unix.IN_ISDIR): "DELETE_SELF DIR", + (unix.IN_MODIFY | unix.IN_ISDIR): "MODIFY DIR", + (unix.IN_MOVED_FROM | unix.IN_ISDIR): "MOVED_FROM DIR", + (unix.IN_MOVE_SELF | unix.IN_ISDIR): "MODE_SELF DIR", + (unix.IN_OPEN | unix.IN_ISDIR): "OPEN DIR", +} + +type Event struct { + name string + op string +} + +func (e Event) String() string { + return fmt.Sprintf("%20s | %s", e.op, e.name) +} + +func newEvent(name string, mask uint32) Event { + e := Event{name: name} + op, ok := InotifyEvents[mask] + if !ok { + op = strconv.FormatInt(int64(mask), 2) + } + e.op = op + return e +} + +func eventLogger(i *Inotify, buffers chan bufRead) { + for bf := range buffers { + n := bf.n + buf := bf.buf + + if n < unix.SizeofInotifyEvent { + // incomplete or erroneous read + continue + } + + var ptr uint32 + var name string + for ptr <= uint32(n-unix.SizeofInotifyEvent) { + sys := (*unix.InotifyEvent)(unsafe.Pointer(&buf[ptr])) + ptr += unix.SizeofInotifyEvent + + watcher, ok := i.watchers[int(sys.Wd)] + if !ok { + continue + } + name = watcher.dir + "/" + if sys.Len > 0 && len(buf) >= int(ptr+sys.Len) { + name += string(bytes.TrimRight(buf[ptr:ptr+sys.Len], "\x00")) + ptr += sys.Len + } + + ev := newEvent(name, sys.Mask) + log.Printf("\x1b[32;1mFS: %+v\x1b[0m", ev) + } + } +} diff --git a/internal/inotify/inotify.go b/internal/inotify/inotify.go index f6431bb..d9d63fb 100644 --- a/internal/inotify/inotify.go +++ b/internal/inotify/inotify.go @@ -2,9 +2,6 @@ package inotify import ( "fmt" - "log" - "strings" - "unsafe" "golang.org/x/sys/unix" ) @@ -57,6 +54,10 @@ func (i *Inotify) UnPause() { i.paused = false } +func (i *Inotify) NumWatchers() int { + return len(i.watchers) +} + func (i *Inotify) String() string { if len(i.watchers) < 20 { dirs := make([]string, 0) @@ -75,9 +76,9 @@ type bufRead struct { } func watch(i *Inotify) { - buf := make([]byte, 20*unix.SizeofInotifyEvent) + buf := make([]byte, 5*unix.SizeofInotifyEvent) buffers := make(chan bufRead) - go verboseWatcher(i, buffers) + go eventLogger(i, buffers) for { n, _ := unix.Read(i.fd, buf) if !i.paused { @@ -89,73 +90,3 @@ func watch(i *Inotify) { } } } - -func verboseWatcher(i *Inotify, buffers chan bufRead) { - for bf := range buffers { - n := bf.n - buf := bf.buf - - if n < unix.SizeofInotifyEvent { - if n == 0 { - // If EOF is received. This should really never happen. - panic(fmt.Sprintf("No bytes read from fd")) - } else if n < 0 { - // If an error occurred while reading. - log.Printf("ERROR: reading from inotify: %d", n) - } else { - // Read was too short. - log.Printf("ERROR: Short read") - } - continue - } - - var offset uint32 - for offset <= uint32(n-unix.SizeofInotifyEvent) { - raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) - - mask := uint32(raw.Mask) - nameLen := uint32(raw.Len) - - name := i.watchers[int(raw.Wd)].dir - if nameLen > 0 { - bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent])) - if uint32(len(bytes)) > nameLen { - name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") - } - } - ev := newEvent(name, mask) - log.Printf("### %+v", ev) - - offset += unix.SizeofInotifyEvent + nameLen - } - } -} - -type Event struct { - name string - op string -} - -func (e Event) String() string { - return fmt.Sprintf("%10s | %s", e.op, e.name) -} - -func newEvent(name string, mask uint32) Event { - e := Event{name: name} - if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { - e.op = "CREATE" - } - if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE { - e.op = "REMOVE" - } - if mask&unix.IN_MODIFY == unix.IN_MODIFY { - e.op = "WRITE" - } - if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { - e.op = "RENAME" - } - if mask&unix.IN_ATTRIB == unix.IN_ATTRIB { - e.op = "CHMOD" - } - return e -} diff --git a/internal/process/process.go b/internal/process/process.go index 50cd097..3bd8265 100644 --- a/internal/process/process.go +++ b/internal/process/process.go @@ -5,28 +5,49 @@ import ( "io/ioutil" "log" "strconv" + "strings" ) type ProcList map[int]string -// type Proc struct { -// Cmd string -// User string -// } - func NewProcList() *ProcList { pl := make(ProcList) return &pl } func (pl ProcList) Refresh() error { + pids, err := getPIDs() + if err != nil { + return err + } + + for i := len(pids) - 1; i >= 0; i-- { + pid := pids[i] + _, ok := pl[pid] + if !ok { + cmd, err := getCmd(pid) + if err != nil { + cmd = "???" // process probably terminated + } + uid, err := getUID(pid) + if err != nil { + uid = "???" + } + log.Printf("\x1b[31;1mCMD: UID=%-4s PID=%-6d | %s\x1b[0m\n", uid, pid, cmd) + pl[pid] = cmd + } + } + + return nil +} + +func getPIDs() ([]int, error) { proc, err := ioutil.ReadDir("/proc") if err != nil { - return fmt.Errorf("opening proc dir: %v", err) + return nil, fmt.Errorf("opening proc dir: %v", err) } pids := make([]int, 0) - for _, f := range proc { if f.IsDir() { name := f.Name() @@ -37,20 +58,7 @@ func (pl ProcList) Refresh() error { pids = append(pids, pid) } } - - for i := len(pids) - 1; i >= 0; i-- { - pid := pids[i] - _, ok := pl[pid] - if !ok { - cmd, err := getCmd(pid) - if err != nil { - cmd = "UNKNOWN" // process probably terminated - } - log.Printf("New process: %5d: %s\n", pid, cmd) - pl[pid] = cmd - } - } - return nil + return pids, nil } func getCmd(pid int) (string, error) { @@ -66,3 +74,20 @@ func getCmd(pid int) (string, error) { } return string(cmd), nil } + +func getUID(pid int) (string, error) { + statPath := fmt.Sprintf("/proc/%d/status", pid) + stat, err := ioutil.ReadFile(statPath) + if err != nil { + return "", err + } + lines := strings.Split(string(stat), "\n") + if len(lines) < 9 { + return "", fmt.Errorf("no uid information") + } + uidL := strings.Split(lines[8], "\t") + if len(uidL) < 2 { + return "", fmt.Errorf("uid line read incomplete") + } + return uidL[1], nil +} diff --git a/internal/walker/walker.go b/internal/walker/walker.go index c1f5f3b..6c8b285 100644 --- a/internal/walker/walker.go +++ b/internal/walker/walker.go @@ -3,45 +3,40 @@ package walker import ( "fmt" "io/ioutil" + "path/filepath" ) -func Walk(root string) (dirCh chan string, errCh chan error) { +func Walk(root string, depth int) (dirCh chan string, errCh chan error, doneCh chan struct{}) { dirCh = make(chan string) errCh = make(chan error) - dirs := make([]string, 1) - dirs[0] = root + doneCh = make(chan struct{}) go func() { - dirCh <- root - }() - go func() { - for { - if len(dirs) == 0 { - break - } - dirs = descent(dirs, dirCh, errCh) - } + descent(root, depth-1, dirCh, errCh, doneCh) close(dirCh) - close(errCh) }() - return dirCh, errCh + return dirCh, errCh, doneCh } -func descent(dirs []string, dirCh chan string, errCh chan error) []string { - next := make([]string, 0) - for _, dir := range dirs { - ls, err := ioutil.ReadDir(dir) - if err != nil { - errCh <- fmt.Errorf("opening dir %s: %v", dir, err) - } +func descent(dir string, depth int, dirCh chan string, errCh chan error, doneCh chan struct{}) { + select { + case dirCh <- dir: + case <-doneCh: + return + } + if depth < 0 { + return + } - for _, e := range ls { - if e.IsDir() { - newDir := dir + e.Name() + "/" - dirCh <- newDir - next = append(next, newDir) - } + ls, err := ioutil.ReadDir(dir) + if err != nil { + errCh <- fmt.Errorf("opening dir %s: %v", dir, err) + } + + for _, e := range ls { + if e.IsDir() { + newDir := filepath.Join(dir, e.Name()) + descent(newDir, depth-1, dirCh, errCh, doneCh) } } - return next }