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"
)
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() {
log.Print("Started")
@ -67,7 +53,9 @@ func main() {
mime.AddExtensionType(".json", "application/json")
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)
httpd := NewHTTPServer(*base, server)

View File

@ -8,5 +8,5 @@ func TestEverything(t *testing.T) {
state := NewTestState()
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")
}
// Update refreshes internal state.
// refresh refreshes internal state.
// It looks for changes to the directory listing, and caches any new mothballs.
func (m *Mothballs) Update() {
func (m *Mothballs) refresh() {
m.categoryLock.Lock()
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) {
m := NewMothballs(new(afero.MemMapFs))
m.createMothball("test1")
m.Update()
m.refresh()
if _, ok := m.categories["test1"]; !ok {
t.Error("Didn't create a new category")
}
@ -54,7 +54,7 @@ func TestMothballs(t *testing.T) {
m.createMothball("test2")
m.Fs.Remove("test1.mb")
m.Update()
m.refresh()
inv = m.Inventory()
if len(inv) != 1 {
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)
Inventory() []Category
CheckAnswer(cat string, points int, answer string) error
Provider
Maintainer
}
// ThemeProvider defines what's required to provide a theme.
type ThemeProvider interface {
Open(path string) (ReadSeekCloser, time.Time, error)
Provider
Maintainer
}
// StateProvider defines what's required to provide MOTH state.
@ -54,12 +54,15 @@ type StateProvider interface {
TeamName(teamID string) (string, error)
SetTeamName(teamID, teamName string) error
AwardPoints(teamID string, cat string, points int) error
Provider
Maintainer
}
// Provider defines providers that can be updated.
type Provider interface {
Update()
// Maintainer is something that can be maintained.
type Maintainer interface {
// 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.
@ -113,8 +116,7 @@ func (mh *MothRequestHandler) ThemeOpen(path string) (ReadSeekCloser, time.Time,
// Register associates a team name with a team ID.
func (mh *MothRequestHandler) Register(teamName string) error {
// XXX: Should we just return success if the team is already registered?
// XXX: Should this function be renamed to Login?
// BUG(neale): Register returns an error if a team is already registered; it may make more sense to return success
if teamName == "" {
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.
type State struct {
afero.Fs
// Enabled tracks whether the current State system is processing updates
Enabled bool
refreshNow chan bool
eventStream chan string
eventWriter afero.File
}
// NewState returns a new State struct backed by the given Fs
func NewState(fs afero.Fs) *State {
return &State{
s := &State{
Fs: fs,
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".
func (s *State) UpdateEnabled() {
// updateEnabled checks a few things to see if this state directory is "enabled".
func (s *State) updateEnabled() {
if _, err := s.Stat("enabled"); os.IsNotExist(err) {
s.Enabled = false
log.Println("Suspended: enabled file missing")
@ -88,17 +100,15 @@ func (s *State) UpdateEnabled() {
// TeamName returns team name given a team ID.
func (s *State) TeamName(teamID string) (string, error) {
// XXX: directory traversal
teamFile := filepath.Join("teams", teamID)
teamNameBytes, err := afero.ReadFile(s, teamFile)
teamName := strings.TrimSpace(string(teamNameBytes))
teamFs := afero.NewBasePathFs(s.Fs, "teams")
teamNameBytes, err := afero.ReadFile(teamFs, teamID)
if os.IsNotExist(err) {
return "", fmt.Errorf("Unregistered team ID: %s", teamID)
} else if err != nil {
return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamID, err)
}
teamName := strings.TrimSpace(string(teamNameBytes))
return teamName, nil
}
@ -194,7 +204,7 @@ func (s *State) AwardPoints(teamID, category string, points int) error {
return err
}
// XXX: update everything
// BUG(neale): When points are awarded, state should be updated immediately
return nil
}
@ -261,10 +271,16 @@ func (s *State) maybeInitialize() {
s.Remove("hours")
s.Remove("points.log")
s.Remove("messages.html")
s.Remove("mothd.log")
s.RemoveAll("points.tmp")
s.RemoveAll("points.new")
s.RemoveAll("teams")
// Open log file
if err := s.reopenEventLog(); err != nil {
log.Fatal(err)
}
// Make sure various subdirectories exist
s.Mkdir("points.tmp", 0755)
s.Mkdir("points.new", 0755)
@ -319,14 +335,55 @@ func (s *State) maybeInitialize() {
if f, err := s.Create("points.log"); err == nil {
f.Close()
}
}
// Update performs housekeeping on a State struct.
func (s *State) Update() {
// LogEvent writes msg to the event log
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.UpdateEnabled()
s.updateEnabled()
if s.Enabled {
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"
"os"
"testing"
"time"
"github.com/spf13/afero"
)
func NewTestState() *State {
s := NewState(new(afero.MemMapFs))
s.Update()
s.refresh()
return s
}
@ -55,7 +56,7 @@ func TestState(t *testing.T) {
category := "poot"
points := 3928
s.AwardPoints(teamID, category, points)
s.Update()
s.refresh()
pl = s.PointsLog()
if len(pl) != 1 {
@ -65,10 +66,34 @@ func TestState(t *testing.T) {
}
s.Fs.Remove("initialized")
s.Update()
s.refresh()
pl = s.PointsLog()
if len(pl) != 0 {
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
}
// Update performs housekeeping for a Theme.
func (t *Theme) Update() {
// Maintain performs housekeeping for a Theme.
func (t *Theme) Maintain(i time.Duration) {
// No periodic tasks for a theme
}