Add event log, not working yet

This commit is contained in:
Neale Pickett 2020-08-18 17:04:23 -06:00
parent 80d91fc6c9
commit 316f44edae
8 changed files with 128 additions and 48 deletions

View File

@ -9,20 +9,6 @@ import (
"github.com/spf13/afero" "github.com/spf13/afero"
) )
func custodian(updateInterval time.Duration, components []Provider) {
update := func() {
for _, c := range components {
c.Update()
}
}
ticker := time.NewTicker(updateInterval)
update()
for range ticker.C {
update()
}
}
func main() { func main() {
log.Print("Started") log.Print("Started")
@ -67,7 +53,9 @@ func main() {
mime.AddExtensionType(".json", "application/json") mime.AddExtensionType(".json", "application/json")
mime.AddExtensionType(".zip", "application/zip") mime.AddExtensionType(".zip", "application/zip")
go custodian(*refreshInterval, []Provider{theme, state, puzzles}) go theme.Maintain(*refreshInterval)
go state.Maintain(*refreshInterval)
go puzzles.Maintain(*refreshInterval)
server := NewMothServer(puzzles, theme, state) server := NewMothServer(puzzles, theme, state)
httpd := NewHTTPServer(*base, server) httpd := NewHTTPServer(*base, server)

View File

@ -8,5 +8,5 @@ func TestEverything(t *testing.T) {
state := NewTestState() state := NewTestState()
t.Error("No test") t.Error("No test")
state.Update() state.refresh()
} }

View File

@ -109,9 +109,9 @@ func (m *Mothballs) CheckAnswer(cat string, points int, answer string) error {
return fmt.Errorf("Invalid answer") return fmt.Errorf("Invalid answer")
} }
// Update refreshes internal state. // refresh refreshes internal state.
// It looks for changes to the directory listing, and caches any new mothballs. // It looks for changes to the directory listing, and caches any new mothballs.
func (m *Mothballs) Update() { func (m *Mothballs) refresh() {
m.categoryLock.Lock() m.categoryLock.Lock()
defer m.categoryLock.Unlock() defer m.categoryLock.Unlock()
@ -169,3 +169,11 @@ func (m *Mothballs) Update() {
} }
} }
} }
// Maintain performs housekeeping for Mothballs.
func (m *Mothballs) Maintain(updateInterval time.Duration) {
m.refresh()
for range time.NewTicker(updateInterval).C {
m.refresh()
}
}

View File

@ -32,7 +32,7 @@ func (m *Mothballs) createMothball(cat string) {
func TestMothballs(t *testing.T) { func TestMothballs(t *testing.T) {
m := NewMothballs(new(afero.MemMapFs)) m := NewMothballs(new(afero.MemMapFs))
m.createMothball("test1") m.createMothball("test1")
m.Update() m.refresh()
if _, ok := m.categories["test1"]; !ok { if _, ok := m.categories["test1"]; !ok {
t.Error("Didn't create a new category") t.Error("Didn't create a new category")
} }
@ -54,7 +54,7 @@ func TestMothballs(t *testing.T) {
m.createMothball("test2") m.createMothball("test2")
m.Fs.Remove("test1.mb") m.Fs.Remove("test1.mb")
m.Update() m.refresh()
inv = m.Inventory() inv = m.Inventory()
if len(inv) != 1 { if len(inv) != 1 {
t.Error("Deleted mothball is still around", inv) t.Error("Deleted mothball is still around", inv)

View File

@ -38,13 +38,13 @@ type PuzzleProvider interface {
Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error) Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error)
Inventory() []Category Inventory() []Category
CheckAnswer(cat string, points int, answer string) error CheckAnswer(cat string, points int, answer string) error
Provider Maintainer
} }
// ThemeProvider defines what's required to provide a theme. // ThemeProvider defines what's required to provide a theme.
type ThemeProvider interface { type ThemeProvider interface {
Open(path string) (ReadSeekCloser, time.Time, error) Open(path string) (ReadSeekCloser, time.Time, error)
Provider Maintainer
} }
// StateProvider defines what's required to provide MOTH state. // StateProvider defines what's required to provide MOTH state.
@ -54,12 +54,15 @@ type StateProvider interface {
TeamName(teamID string) (string, error) TeamName(teamID string) (string, error)
SetTeamName(teamID, teamName string) error SetTeamName(teamID, teamName string) error
AwardPoints(teamID string, cat string, points int) error AwardPoints(teamID string, cat string, points int) error
Provider Maintainer
} }
// Provider defines providers that can be updated. // Maintainer is something that can be maintained.
type Provider interface { type Maintainer interface {
Update() // Maintain is the maintenance loop.
// It will only be called once, when execution begins.
// It's okay to just exit if there's no maintenance to be done.
Maintain(updateInterval time.Duration)
} }
// MothServer gathers together the providers that make up a MOTH server. // MothServer gathers together the providers that make up a MOTH server.
@ -113,8 +116,7 @@ func (mh *MothRequestHandler) ThemeOpen(path string) (ReadSeekCloser, time.Time,
// Register associates a team name with a team ID. // Register associates a team name with a team ID.
func (mh *MothRequestHandler) Register(teamName string) error { func (mh *MothRequestHandler) Register(teamName string) error {
// XXX: Should we just return success if the team is already registered? // BUG(neale): Register returns an error if a team is already registered; it may make more sense to return success
// XXX: Should this function be renamed to Login?
if teamName == "" { if teamName == "" {
return fmt.Errorf("Empty team name") return fmt.Errorf("Empty team name")
} }

View File

@ -24,19 +24,31 @@ const DistinguishableChars = "234678abcdefhikmnpqrtwxyz="
// The only thing State methods need to know is the path to the state directory. // The only thing State methods need to know is the path to the state directory.
type State struct { type State struct {
afero.Fs afero.Fs
// Enabled tracks whether the current State system is processing updates
Enabled bool Enabled bool
refreshNow chan bool
eventStream chan string
eventWriter afero.File
} }
// NewState returns a new State struct backed by the given Fs // NewState returns a new State struct backed by the given Fs
func NewState(fs afero.Fs) *State { func NewState(fs afero.Fs) *State {
return &State{ s := &State{
Fs: fs, Fs: fs,
Enabled: true, Enabled: true,
refreshNow: make(chan bool, 5),
eventStream: make(chan string, 80),
} }
if err := s.reopenEventLog(); err != nil {
log.Fatal(err)
}
return s
} }
// UpdateEnabled checks a few things to see if this state directory is "enabled". // updateEnabled checks a few things to see if this state directory is "enabled".
func (s *State) UpdateEnabled() { func (s *State) updateEnabled() {
if _, err := s.Stat("enabled"); os.IsNotExist(err) { if _, err := s.Stat("enabled"); os.IsNotExist(err) {
s.Enabled = false s.Enabled = false
log.Println("Suspended: enabled file missing") log.Println("Suspended: enabled file missing")
@ -88,17 +100,15 @@ func (s *State) UpdateEnabled() {
// TeamName returns team name given a team ID. // TeamName returns team name given a team ID.
func (s *State) TeamName(teamID string) (string, error) { func (s *State) TeamName(teamID string) (string, error) {
// XXX: directory traversal teamFs := afero.NewBasePathFs(s.Fs, "teams")
teamFile := filepath.Join("teams", teamID) teamNameBytes, err := afero.ReadFile(teamFs, teamID)
teamNameBytes, err := afero.ReadFile(s, teamFile)
teamName := strings.TrimSpace(string(teamNameBytes))
if os.IsNotExist(err) { if os.IsNotExist(err) {
return "", fmt.Errorf("Unregistered team ID: %s", teamID) return "", fmt.Errorf("Unregistered team ID: %s", teamID)
} else if err != nil { } else if err != nil {
return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamID, err) return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamID, err)
} }
teamName := strings.TrimSpace(string(teamNameBytes))
return teamName, nil return teamName, nil
} }
@ -194,7 +204,7 @@ func (s *State) AwardPoints(teamID, category string, points int) error {
return err return err
} }
// XXX: update everything // BUG(neale): When points are awarded, state should be updated immediately
return nil return nil
} }
@ -261,10 +271,16 @@ func (s *State) maybeInitialize() {
s.Remove("hours") s.Remove("hours")
s.Remove("points.log") s.Remove("points.log")
s.Remove("messages.html") s.Remove("messages.html")
s.Remove("mothd.log")
s.RemoveAll("points.tmp") s.RemoveAll("points.tmp")
s.RemoveAll("points.new") s.RemoveAll("points.new")
s.RemoveAll("teams") s.RemoveAll("teams")
// Open log file
if err := s.reopenEventLog(); err != nil {
log.Fatal(err)
}
// Make sure various subdirectories exist // Make sure various subdirectories exist
s.Mkdir("points.tmp", 0755) s.Mkdir("points.tmp", 0755)
s.Mkdir("points.new", 0755) s.Mkdir("points.new", 0755)
@ -319,14 +335,55 @@ func (s *State) maybeInitialize() {
if f, err := s.Create("points.log"); err == nil { if f, err := s.Create("points.log"); err == nil {
f.Close() f.Close()
} }
} }
// Update performs housekeeping on a State struct. // LogEvent writes msg to the event log
func (s *State) Update() { func (s *State) LogEvent(msg string) {
s.eventStream <- msg
}
// LogEventf writes a formatted message to the event log
func (s *State) LogEventf(format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...)
s.LogEvent(msg)
}
func (s *State) reopenEventLog() error {
if s.eventWriter != nil {
if err := s.eventWriter.Close(); err != nil {
// We're going to soldier on if Close returns error
log.Print(err)
}
}
eventWriter, err := s.OpenFile("events.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
s.eventWriter = eventWriter
return nil
}
func (s *State) refresh() {
s.maybeInitialize() s.maybeInitialize()
s.UpdateEnabled() s.updateEnabled()
if s.Enabled { if s.Enabled {
s.collectPoints() s.collectPoints()
} }
} }
// Maintain performs housekeeping on a State struct.
func (s *State) Maintain(updateInterval time.Duration) {
ticker := time.NewTicker(updateInterval)
s.refresh()
for {
select {
case msg := <-s.eventStream:
fmt.Println(s.eventWriter, time.Now().Unix(), msg)
s.eventWriter.Sync()
case <-ticker.C:
s.refresh()
case <-s.refreshNow:
s.refresh()
}
}
}

View File

@ -4,13 +4,14 @@ import (
"bytes" "bytes"
"os" "os"
"testing" "testing"
"time"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
func NewTestState() *State { func NewTestState() *State {
s := NewState(new(afero.MemMapFs)) s := NewState(new(afero.MemMapFs))
s.Update() s.refresh()
return s return s
} }
@ -55,7 +56,7 @@ func TestState(t *testing.T) {
category := "poot" category := "poot"
points := 3928 points := 3928
s.AwardPoints(teamID, category, points) s.AwardPoints(teamID, category, points)
s.Update() s.refresh()
pl = s.PointsLog() pl = s.PointsLog()
if len(pl) != 1 { if len(pl) != 1 {
@ -65,10 +66,34 @@ func TestState(t *testing.T) {
} }
s.Fs.Remove("initialized") s.Fs.Remove("initialized")
s.Update() s.refresh()
pl = s.PointsLog() pl = s.PointsLog()
if len(pl) != 0 { if len(pl) != 0 {
t.Errorf("After reinitialization, points log has length %d", len(pl)) t.Errorf("After reinitialization, points log has length %d", len(pl))
} }
} }
func TestStateEvents(t *testing.T) {
s := NewTestState()
s.LogEvent("moo")
s.LogEventf("moo %d", 2)
if msg := <-s.eventStream; msg != "moo" {
t.Error("Wrong message from event stream", msg)
}
if msg := <-s.eventStream; msg != "moo 2" {
t.Error("Formatted event is wrong:", msg)
}
}
func TestStateMaintainer(t *testing.T) {
s := NewTestState()
go s.Maintain(2 * time.Second)
s.LogEvent("Hello!")
eventLog, _ := afero.ReadFile(s.Fs, "event.log")
if len(eventLog) != 12 {
t.Error("Wrong event log length:", len(eventLog))
}
}

View File

@ -34,7 +34,7 @@ func (t *Theme) Open(name string) (ReadSeekCloser, time.Time, error) {
return f, fi.ModTime(), nil return f, fi.ModTime(), nil
} }
// Update performs housekeeping for a Theme. // Maintain performs housekeeping for a Theme.
func (t *Theme) Update() { func (t *Theme) Maintain(i time.Duration) {
// No periodic tasks for a theme // No periodic tasks for a theme
} }