mirror of https://github.com/dirtbags/moth.git
Add event log, not working yet
This commit is contained in:
parent
80d91fc6c9
commit
316f44edae
|
@ -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)
|
||||
|
|
|
@ -8,5 +8,5 @@ func TestEverything(t *testing.T) {
|
|||
state := NewTestState()
|
||||
t.Error("No test")
|
||||
|
||||
state.Update()
|
||||
state.refresh()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue