implement throttling, refactor everything
parent
bb69e9c613
commit
c05634606e
|
@ -1,107 +1,74 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"io"
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"time"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO flesh out event handling with an eye towards eventual sampling
|
type EventType int
|
||||||
|
|
||||||
func initHomeWatcher() (*fsnotify.Watcher, error) {
|
const (
|
||||||
fmt.Fprintf(os.Stderr, "setting up home watcher...\n")
|
eventHomeActivity = iota
|
||||||
watcher, err := fsnotify.NewWatcher()
|
eventLogin
|
||||||
if err != nil {
|
eventLogout
|
||||||
return nil, err
|
)
|
||||||
}
|
|
||||||
|
|
||||||
out, err := exec.Command("sh", "-c", "stats | jq .active_users[] | tr -d '\"'").Output()
|
// TODO may compute flavor externally based on type + username...
|
||||||
if err != nil {
|
type Event struct {
|
||||||
return nil, fmt.Errorf("failed to call and process stats: %w", err)
|
Username string
|
||||||
}
|
Type EventType
|
||||||
|
Flavor string
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(out))
|
|
||||||
for scanner.Scan() {
|
|
||||||
username := strings.TrimSpace(scanner.Text())
|
|
||||||
home := filepath.Join("/home", username)
|
|
||||||
addHome(watcher, home)
|
|
||||||
}
|
|
||||||
|
|
||||||
return watcher, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func addHome(watcher *fsnotify.Watcher, homePath string) error {
|
func (e Event) String() string {
|
||||||
fileCount := 0
|
return e.Flavor
|
||||||
filepath.Walk(homePath, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if info.IsDir() && strings.HasPrefix(info.Name(), ".") {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
fileCount++
|
|
||||||
if info.Mode()&os.ModeSymlink != 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = watcher.Add(path)
|
|
||||||
if err != nil && err.Error() != "permission denied" && err.Error() != "no such file or directory" {
|
|
||||||
fmt.Printf("%#v\n", err)
|
|
||||||
fmt.Println("Died at ", fileCount)
|
|
||||||
fmt.Printf("%#v\n", info)
|
|
||||||
panic(err.Error())
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(os.Stderr, "watching %s\n", path)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func watchHomes(watcher *fsnotify.Watcher) {
|
func NewLogger(w io.Writer) *log.Logger {
|
||||||
fmt.Fprintf(os.Stderr, "starting poll\n")
|
// TODO consider https://github.com/lestrrat-go/file-rotatelogs
|
||||||
for {
|
return log.New(w, "", log.LstdFlags)
|
||||||
select {
|
|
||||||
case event, ok := <-watcher.Events:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println("event:", event)
|
|
||||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
|
||||||
log.Println("modified file:", event.Name)
|
|
||||||
}
|
|
||||||
case err, ok := <-watcher.Errors:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println("error:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func cli(args []string) int {
|
func cli(args []string) int {
|
||||||
fmt.Fprintf(os.Stderr, "starting\n")
|
// TODO less hardcoded
|
||||||
watcher, err := initHomeWatcher()
|
lf, err := os.OpenFile("bustle.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
fmt.Fprintf(os.Stderr, "watcher initialized\n")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to create watcher: %w\n", err)
|
log.Printf("failed to open log file: %s\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
logger := NewLogger(lf)
|
||||||
|
|
||||||
|
rawEvents := make(chan Event)
|
||||||
|
throttler := NewThrottler(time.Hour)
|
||||||
|
|
||||||
|
hw, err := NewHomeWatcher(rawEvents)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to create hw: %s\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
defer watcher.Close()
|
defer hw.Close()
|
||||||
|
|
||||||
// TODO have an event bus to write to
|
// need a channel of raw events that can take home watcher events + eventual other events like
|
||||||
// TODO what is this done for do i need this done
|
// logging in
|
||||||
done := make(chan bool)
|
// listener grabs each raw event and checks throttler
|
||||||
go watchHomes(watcher)
|
// if not throttled, event is written to log file
|
||||||
<-done
|
|
||||||
|
go hw.Watch()
|
||||||
|
for {
|
||||||
|
// TODO prob use a select with an error channel, like fsnotify does
|
||||||
|
event, ok := <-rawEvents
|
||||||
|
if !ok {
|
||||||
|
log.Println("event error")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if throttler.Throttled(event.Username) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
throttler.Touch(event.Username)
|
||||||
|
logger.Println(event)
|
||||||
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@ -109,3 +76,8 @@ func cli(args []string) int {
|
||||||
func main() {
|
func main() {
|
||||||
os.Exit(cli(os.Args))
|
os.Exit(cli(os.Args))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
note to self: restarting the bustled nightly will fix a lot of presence problems. Instead of worrying about people creating directories and logging off, i can just do a daily restart. i /do/ want to capture people logging in, though, but that's not as hard as keeping track of files.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Throttler interface {
|
||||||
|
Touch(string)
|
||||||
|
Throttled(string) bool
|
||||||
|
SetInterval(time.Duration)
|
||||||
|
Now() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type MapThrottler struct {
|
||||||
|
interval time.Duration
|
||||||
|
users map[string]time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewThrottler(interval time.Duration) Throttler {
|
||||||
|
return &MapThrottler{
|
||||||
|
interval: interval,
|
||||||
|
users: map[string]time.Time{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MapThrottler) SetInterval(interval time.Duration) {
|
||||||
|
t.interval = interval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MapThrottler) Touch(username string) {
|
||||||
|
t.users[username] = t.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MapThrottler) Now() time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MapThrottler) Throttled(username string) bool {
|
||||||
|
lastTouch, ok := t.users[username]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return t.Now().Sub(lastTouch) < t.interval
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Watcher interface {
|
||||||
|
AddDirectory(string) error
|
||||||
|
Watch()
|
||||||
|
Close()
|
||||||
|
Root() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HomeWatcher struct {
|
||||||
|
watcher *fsnotify.Watcher
|
||||||
|
root string
|
||||||
|
raw chan Event
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHomeWatcher(rawEvents chan Event) (Watcher, error) {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hw := &HomeWatcher{
|
||||||
|
watcher: watcher,
|
||||||
|
root: "/home",
|
||||||
|
raw: rawEvents,
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := exec.Command("sh", "-c", "stats | jq .active_users[] | tr -d '\"'").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to call and process stats: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(out))
|
||||||
|
for scanner.Scan() {
|
||||||
|
username := strings.TrimSpace(scanner.Text())
|
||||||
|
home := filepath.Join(hw.Root(), username)
|
||||||
|
if err := hw.AddDirectory(home); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hw *HomeWatcher) AddDirectory(path string) error {
|
||||||
|
fileCount := 0
|
||||||
|
filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if info.IsDir() && strings.HasPrefix(info.Name(), ".") {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
fileCount++
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = hw.watcher.Add(path)
|
||||||
|
if err != nil && err.Error() != "permission denied" && err.Error() != "no such file or directory" {
|
||||||
|
return fmt.Errorf("died at file %d. info: %#v error: %w\n", fileCount, info, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("watching %s\n", path)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hw *HomeWatcher) Root() string {
|
||||||
|
return hw.root
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hw *HomeWatcher) Close() {
|
||||||
|
hw.watcher.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hw *HomeWatcher) Watch() {
|
||||||
|
unre := regexp.MustCompile("^/home/([^/]+)/.*$")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-hw.watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
matches := unre.FindStringSubmatch(event.Name)
|
||||||
|
if matches == nil {
|
||||||
|
log.Printf("could not extract username: %s\n", event)
|
||||||
|
}
|
||||||
|
un := matches[1]
|
||||||
|
hw.raw <- Event{
|
||||||
|
Username: un,
|
||||||
|
Type: eventHomeActivity,
|
||||||
|
Flavor: fmt.Sprintf("%s is knocking about in %s", un, path.Dir(event.Name)),
|
||||||
|
}
|
||||||
|
// event.Name is filepath
|
||||||
|
log.Println("event:", event)
|
||||||
|
fmt.Printf("DBG %#v\n", event)
|
||||||
|
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||||
|
log.Println("modified file:", event.Name)
|
||||||
|
}
|
||||||
|
case err, ok := <-hw.watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue