mirror of https://github.com/dirtbags/moth.git
Breaking out state mechanisms into a more abstract form that shouldbe
easier to extend
This commit is contained in:
parent
0b8a855284
commit
4093797899
|
@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Added
|
||||
- URL parameter to points.json to allow returning only the JSON for a single
|
||||
team by its team id (e.g., points.json?id=abc123).
|
||||
### Changed
|
||||
- Abstract state mechanisms so that it is easier to move to different backends
|
||||
|
||||
## [3.4.2] - 2019-11-18
|
||||
### Fixed
|
||||
|
|
|
@ -0,0 +1,292 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LegacyMOTHState struct {
|
||||
StateDir string
|
||||
update chan bool
|
||||
maintenanceInterval time.Duration
|
||||
}
|
||||
|
||||
func (state *LegacyMOTHState) Initialize() (bool, error) {
|
||||
|
||||
if _, err := os.Stat(state.StateDir); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
state.MaybeInitialize()
|
||||
|
||||
if state.update == nil {
|
||||
state.update = make(chan bool, 10)
|
||||
go state.Maintenance(state.maintenanceInterval)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (state *LegacyMOTHState) login(teamName string, token string) (bool, error) {
|
||||
for a, _ := range state.getTeams() {
|
||||
if a == token {
|
||||
f, err := os.OpenFile(state.StatePath("teams", token), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
if os.IsExist(err) {
|
||||
return true, ErrAlreadyRegistered
|
||||
} else {
|
||||
return false, ErrRegistrationError
|
||||
}
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fmt.Fprintln(f, teamName)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, ErrInvalidTeamID
|
||||
}
|
||||
|
||||
func (state *LegacyMOTHState) StatePath(parts ...string) string {
|
||||
tail := pathCleanse(parts)
|
||||
return path.Join(state.StateDir, tail)
|
||||
}
|
||||
|
||||
func (state *LegacyMOTHState) TeamName(teamId string) (string, error) {
|
||||
teamNameBytes, err := ioutil.ReadFile(state.StatePath("teams", teamId))
|
||||
teamName := strings.TrimSpace(string(teamNameBytes))
|
||||
return teamName, err
|
||||
}
|
||||
|
||||
func (state *LegacyMOTHState) isEnabled() bool {
|
||||
if _, err := os.Stat(state.StatePath("disabled")); err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
untilspec, err := ioutil.ReadFile(state.StatePath("until"))
|
||||
if err == nil {
|
||||
untilspecs := strings.TrimSpace(string(untilspec))
|
||||
until, err := time.Parse(time.RFC3339, untilspecs)
|
||||
if err != nil {
|
||||
log.Printf("Suspended: Unparseable until date: %s", untilspec)
|
||||
return false
|
||||
}
|
||||
if until.Before(time.Now()) {
|
||||
log.Print("Suspended: until time reached, suspending maintenance")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// AwardPoints gives points to teamId in category.
|
||||
// It first checks to make sure these are not duplicate points.
|
||||
// This is not a perfect check, you can trigger a race condition here.
|
||||
// It's just a courtesy to the user.
|
||||
// The maintenance task makes sure we never have duplicate points in the log.
|
||||
func (state *LegacyMOTHState) AwardPoints(teamId, category string, points int) error {
|
||||
a := Award{
|
||||
When: time.Now(),
|
||||
TeamId: teamId,
|
||||
Category: category,
|
||||
Points: points,
|
||||
}
|
||||
|
||||
_, err := state.TeamName(teamId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("No registered team with this hash")
|
||||
}
|
||||
|
||||
for _, e := range state.PointsLog("") {
|
||||
if a.Same(e) {
|
||||
return fmt.Errorf("Points already awarded to this team in this category")
|
||||
}
|
||||
}
|
||||
|
||||
fn := fmt.Sprintf("%s-%s-%d", teamId, category, points)
|
||||
tmpfn := state.StatePath("points.tmp", fn)
|
||||
newfn := state.StatePath("points.new", fn)
|
||||
|
||||
if err := ioutil.WriteFile(tmpfn, []byte(a.String()), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpfn, newfn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state.update <- true
|
||||
log.Printf("Award %s %s %d", teamId, category, points)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (state *LegacyMOTHState) PointsLog(teamId string) []*Award {
|
||||
var ret []*Award
|
||||
|
||||
fn := state.StatePath("points.log")
|
||||
f, err := os.Open(fn)
|
||||
if err != nil {
|
||||
log.Printf("Unable to open %s: %s", fn, err)
|
||||
return ret
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
cur, err := ParseAward(line)
|
||||
if err != nil {
|
||||
log.Printf("Skipping malformed award line %s: %s", line, err)
|
||||
continue
|
||||
}
|
||||
if len(teamId) > 0 && cur.TeamId != teamId {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, cur)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (state *LegacyMOTHState) getConfig(configName string) (string, error) {
|
||||
fn := state.StatePath(configName)
|
||||
data, err := ioutil.ReadFile(fn)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Unable to open %s: %s", fn, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (state *LegacyMOTHState) Maintenance(maintenanceInterval time.Duration) {
|
||||
for {
|
||||
if state.isEnabled() {
|
||||
state.collectPoints()
|
||||
}
|
||||
select {
|
||||
case <-state.update:
|
||||
// log.Print("Forced update")
|
||||
case <-time.After(maintenanceInterval):
|
||||
// log.Print("Housekeeping")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (state *LegacyMOTHState) getTeams() map[string]struct{} {
|
||||
filepath := state.StatePath("teamids.txt")
|
||||
teamids, err := os.Open(filepath)
|
||||
teams := make(map[string]struct{})
|
||||
if err != nil {
|
||||
log.Printf("Error openining %s: %s", filepath, err)
|
||||
return teams
|
||||
}
|
||||
defer teamids.Close()
|
||||
|
||||
// List out team IDs
|
||||
scanner := bufio.NewScanner(teamids)
|
||||
for scanner.Scan() {
|
||||
teamId := scanner.Text()
|
||||
if (teamId == "..") || strings.ContainsAny(teamId, "/") {
|
||||
log.Printf("Dangerous team ID dropped: %s", teamId)
|
||||
continue
|
||||
}
|
||||
teams[scanner.Text()] = struct{}{}
|
||||
//newList = append(newList, scanner.Text())
|
||||
}
|
||||
return teams
|
||||
//return newList
|
||||
}
|
||||
|
||||
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
|
||||
// removing each points.new/ file as it goes.
|
||||
func (state *LegacyMOTHState) collectPoints() {
|
||||
logf, err := os.OpenFile(state.StatePath("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Printf("Can't append to points log: %s", err)
|
||||
return
|
||||
}
|
||||
defer logf.Close()
|
||||
|
||||
files, err := ioutil.ReadDir(state.StatePath("points.new"))
|
||||
if err != nil {
|
||||
log.Printf("Error reading packages: %s", err)
|
||||
}
|
||||
for _, f := range files {
|
||||
filename := state.StatePath("points.new", f.Name())
|
||||
s, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Printf("Can't read points file %s: %s", filename, err)
|
||||
continue
|
||||
}
|
||||
award, err := ParseAward(string(s))
|
||||
if err != nil {
|
||||
log.Printf("Can't parse award file %s: %s", filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
duplicate := false
|
||||
for _, e := range state.PointsLog("") {
|
||||
if award.Same(e) {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if duplicate {
|
||||
log.Printf("Skipping duplicate points: %s", award.String())
|
||||
} else {
|
||||
fmt.Fprintf(logf, "%s\n", award.String())
|
||||
}
|
||||
|
||||
logf.Sync()
|
||||
if err := os.Remove(filename); err != nil {
|
||||
log.Printf("Unable to remove %s: %s", filename, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (state *LegacyMOTHState) MaybeInitialize() {
|
||||
// Only do this if it hasn't already been done
|
||||
if _, err := os.Stat(state.StatePath("initialized")); err == nil {
|
||||
return
|
||||
}
|
||||
log.Printf("initialized file missing, re-initializing")
|
||||
|
||||
// Remove any extant control and state files
|
||||
os.Remove(state.StatePath("until"))
|
||||
os.Remove(state.StatePath("disabled"))
|
||||
os.Remove(state.StatePath("points.log"))
|
||||
os.RemoveAll(state.StatePath("points.tmp"))
|
||||
os.RemoveAll(state.StatePath("points.new"))
|
||||
os.RemoveAll(state.StatePath("teams"))
|
||||
|
||||
// Make sure various subdirectories exist
|
||||
os.Mkdir(state.StatePath("points.tmp"), 0755)
|
||||
os.Mkdir(state.StatePath("points.new"), 0755)
|
||||
os.Mkdir(state.StatePath("teams"), 0755)
|
||||
|
||||
// Preseed available team ids if file doesn't exist
|
||||
if f, err := os.OpenFile(state.StatePath("teamids.txt"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil {
|
||||
defer f.Close()
|
||||
for i := 0; i <= 100; i += 1 {
|
||||
fmt.Fprintln(f, mktoken())
|
||||
}
|
||||
}
|
||||
|
||||
// Create initialized file that signals whether we're set up
|
||||
f, err := os.OpenFile(state.StatePath("initialized"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
defer f.Close()
|
||||
fmt.Fprintln(f, "Remove this file to reinitialize the contest")
|
||||
}
|
|
@ -63,41 +63,34 @@ func (ctx *Instance) registerHandler(w http.ResponseWriter, req *http.Request) {
|
|||
teamName := req.FormValue("name")
|
||||
teamId := req.FormValue("id")
|
||||
|
||||
if !ctx.ValidTeamId(teamId) {
|
||||
success, err := ctx.State.login(teamName, teamId)
|
||||
|
||||
if (success && err == nil) {
|
||||
respond(
|
||||
w, req, JSendSuccess,
|
||||
"Team registered",
|
||||
"Your team has been named and you may begin using your team ID!",
|
||||
)
|
||||
} else if (err == ErrInvalidTeamID) {
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Invalid Team ID",
|
||||
"I don't have a record of that team ID. Maybe you used capital letters accidentally?",
|
||||
)
|
||||
return
|
||||
} else if (err == ErrAlreadyRegistered) {
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Already registered",
|
||||
"This team ID has already been registered.",
|
||||
)
|
||||
} else if (err != nil) {
|
||||
log.Print(err)
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Registration failed",
|
||||
"Unable to register. Perhaps a teammate has already registered?",
|
||||
)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(ctx.StatePath("teams", teamId), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
if os.IsExist(err) {
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Already registered",
|
||||
"This team ID has already been registered.",
|
||||
)
|
||||
} else {
|
||||
log.Print(err)
|
||||
respond(
|
||||
w, req, JSendFail,
|
||||
"Registration failed",
|
||||
"Unable to register. Perhaps a teammate has already registered?",
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fmt.Fprintln(f, teamName)
|
||||
respond(
|
||||
w, req, JSendSuccess,
|
||||
"Team registered",
|
||||
"Your team has been named and you may begin using your team ID!",
|
||||
)
|
||||
}
|
||||
|
||||
func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
||||
|
@ -155,7 +148,7 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := ctx.AwardPoints(teamId, category, points); err != nil {
|
||||
if err := ctx.State.AwardPoints(teamId, category, points); err != nil {
|
||||
respond(
|
||||
w, req, JSendError,
|
||||
"Cannot award points",
|
||||
|
@ -172,7 +165,7 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
func (ctx *Instance) puzzlesHandler(w http.ResponseWriter, req *http.Request) {
|
||||
teamId := req.FormValue("id")
|
||||
if _, err := ctx.TeamName(teamId); err != nil {
|
||||
if _, err := ctx.State.TeamName(teamId); err != nil {
|
||||
http.Error(w, "Must provide team ID", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
@ -262,7 +255,7 @@ func (ctx *Instance) manifestHandler(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
teamId := req.FormValue("id")
|
||||
if _, err := ctx.TeamName(teamId); err != nil {
|
||||
if _, err := ctx.State.TeamName(teamId); err != nil {
|
||||
http.Error(w, "Must provide a valid team ID", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
|
127
src/instance.go
127
src/instance.go
|
@ -1,11 +1,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -24,6 +21,9 @@ type Instance struct {
|
|||
MothballDir string
|
||||
StateDir string
|
||||
ThemeDir string
|
||||
|
||||
State MOTHState
|
||||
|
||||
AttemptInterval time.Duration
|
||||
|
||||
Runtime RuntimeConfig
|
||||
|
@ -43,7 +43,8 @@ func (ctx *Instance) Initialize() error {
|
|||
if _, err := os.Stat(ctx.MothballDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(ctx.StateDir); err != nil {
|
||||
|
||||
if _, err := ctx.State.Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -55,7 +56,6 @@ func (ctx *Instance) Initialize() error {
|
|||
ctx.mux = http.NewServeMux()
|
||||
|
||||
ctx.BindHandlers()
|
||||
ctx.MaybeInitialize()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -72,43 +72,6 @@ func mktoken() string {
|
|||
return string(a)
|
||||
}
|
||||
|
||||
func (ctx *Instance) MaybeInitialize() {
|
||||
// Only do this if it hasn't already been done
|
||||
if _, err := os.Stat(ctx.StatePath("initialized")); err == nil {
|
||||
return
|
||||
}
|
||||
log.Print("initialized file missing, re-initializing")
|
||||
|
||||
// Remove any extant control and state files
|
||||
os.Remove(ctx.StatePath("until"))
|
||||
os.Remove(ctx.StatePath("disabled"))
|
||||
os.Remove(ctx.StatePath("points.log"))
|
||||
os.RemoveAll(ctx.StatePath("points.tmp"))
|
||||
os.RemoveAll(ctx.StatePath("points.new"))
|
||||
os.RemoveAll(ctx.StatePath("teams"))
|
||||
|
||||
// Make sure various subdirectories exist
|
||||
os.Mkdir(ctx.StatePath("points.tmp"), 0755)
|
||||
os.Mkdir(ctx.StatePath("points.new"), 0755)
|
||||
os.Mkdir(ctx.StatePath("teams"), 0755)
|
||||
|
||||
// Preseed available team ids if file doesn't exist
|
||||
if f, err := os.OpenFile(ctx.StatePath("teamids.txt"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil {
|
||||
defer f.Close()
|
||||
for i := 0; i <= 100; i += 1 {
|
||||
fmt.Fprintln(f, mktoken())
|
||||
}
|
||||
}
|
||||
|
||||
// Create initialized file that signals whether we're set up
|
||||
f, err := os.OpenFile(ctx.StatePath("initialized"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
defer f.Close()
|
||||
fmt.Fprintln(f, "Remove this file to reinitialize the contest")
|
||||
}
|
||||
|
||||
func pathCleanse(parts []string) string {
|
||||
clean := make([]string, len(parts))
|
||||
for i := range parts {
|
||||
|
@ -127,11 +90,6 @@ func (ctx Instance) MothballPath(parts ...string) string {
|
|||
return path.Join(ctx.MothballDir, tail)
|
||||
}
|
||||
|
||||
func (ctx *Instance) StatePath(parts ...string) string {
|
||||
tail := pathCleanse(parts)
|
||||
return path.Join(ctx.StateDir, tail)
|
||||
}
|
||||
|
||||
func (ctx *Instance) ThemePath(parts ...string) string {
|
||||
tail := pathCleanse(parts)
|
||||
return path.Join(ctx.ThemeDir, tail)
|
||||
|
@ -151,75 +109,6 @@ func (ctx *Instance) TooFast(teamId string) bool {
|
|||
return now.Before(next)
|
||||
}
|
||||
|
||||
func (ctx *Instance) PointsLog(teamId string) []*Award {
|
||||
var ret []*Award
|
||||
|
||||
fn := ctx.StatePath("points.log")
|
||||
f, err := os.Open(fn)
|
||||
if err != nil {
|
||||
log.Printf("Unable to open %s: %s", fn, err)
|
||||
return ret
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
cur, err := ParseAward(line)
|
||||
if err != nil {
|
||||
log.Printf("Skipping malformed award line %s: %s", line, err)
|
||||
continue
|
||||
}
|
||||
if len(teamId) > 0 && cur.TeamId != teamId {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, cur)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// AwardPoints gives points to teamId in category.
|
||||
// It first checks to make sure these are not duplicate points.
|
||||
// This is not a perfect check, you can trigger a race condition here.
|
||||
// It's just a courtesy to the user.
|
||||
// The maintenance task makes sure we never have duplicate points in the log.
|
||||
func (ctx *Instance) AwardPoints(teamId, category string, points int) error {
|
||||
a := Award{
|
||||
When: time.Now(),
|
||||
TeamId: teamId,
|
||||
Category: category,
|
||||
Points: points,
|
||||
}
|
||||
|
||||
_, err := ctx.TeamName(teamId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("No registered team with this hash")
|
||||
}
|
||||
|
||||
for _, e := range ctx.PointsLog("") {
|
||||
if a.Same(e) {
|
||||
return fmt.Errorf("Points already awarded to this team in this category")
|
||||
}
|
||||
}
|
||||
|
||||
fn := fmt.Sprintf("%s-%s-%d", teamId, category, points)
|
||||
tmpfn := ctx.StatePath("points.tmp", fn)
|
||||
newfn := ctx.StatePath("points.new", fn)
|
||||
|
||||
if err := ioutil.WriteFile(tmpfn, []byte(a.String()), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpfn, newfn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.update <- true
|
||||
log.Printf("Award %s %s %d", teamId, category, points)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctx *Instance) OpenCategoryFile(category string, parts ...string) (io.ReadCloser, error) {
|
||||
mb, ok := ctx.categories[category]
|
||||
if !ok {
|
||||
|
@ -238,9 +127,3 @@ func (ctx *Instance) ValidTeamId(teamId string) bool {
|
|||
|
||||
return ok
|
||||
}
|
||||
|
||||
func (ctx *Instance) TeamName(teamId string) (string, error) {
|
||||
teamNameBytes, err := ioutil.ReadFile(ctx.StatePath("teams", teamId))
|
||||
teamName := strings.TrimSpace(string(teamNameBytes))
|
||||
return teamName, err
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -28,7 +26,7 @@ func (pm *PuzzleMap) MarshalJSON() ([]byte, error) {
|
|||
|
||||
func (ctx *Instance) generatePuzzleList() {
|
||||
maxByCategory := map[string]int{}
|
||||
for _, a := range ctx.PointsLog("") {
|
||||
for _, a := range ctx.State.PointsLog("") {
|
||||
if a.Points > maxByCategory[a.Category] {
|
||||
maxByCategory[a.Category] = a.Points
|
||||
}
|
||||
|
@ -73,13 +71,13 @@ func (ctx *Instance) generatePointsLog(teamId string) []byte {
|
|||
Points []*Award `json:"points"`
|
||||
}
|
||||
ret.Teams = map[string]string{}
|
||||
ret.Points = ctx.PointsLog(teamId)
|
||||
ret.Points = ctx.State.PointsLog(teamId)
|
||||
|
||||
teamNumbersById := map[string]int{}
|
||||
for nr, a := range ret.Points {
|
||||
teamNumber, ok := teamNumbersById[a.TeamId]
|
||||
if !ok {
|
||||
teamName, err := ctx.TeamName(a.TeamId)
|
||||
teamName, err := ctx.State.TeamName(a.TeamId)
|
||||
if err != nil {
|
||||
teamName = "Rodney" // https://en.wikipedia.org/wiki/Rogue_(video_game)#Gameplay
|
||||
}
|
||||
|
@ -95,7 +93,7 @@ func (ctx *Instance) generatePointsLog(teamId string) []byte {
|
|||
log.Printf("Marshalling points.js: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if len(teamId) == 0 {
|
||||
ctx.jPointsLog = jpl
|
||||
}
|
||||
|
@ -105,7 +103,7 @@ func (ctx *Instance) generatePointsLog(teamId string) []byte {
|
|||
// maintenance runs
|
||||
func (ctx *Instance) tidy() {
|
||||
// Do they want to reset everything?
|
||||
ctx.MaybeInitialize()
|
||||
ctx.State.Initialize()
|
||||
|
||||
// Check set config
|
||||
ctx.UpdateConfig()
|
||||
|
@ -148,37 +146,19 @@ func (ctx *Instance) tidy() {
|
|||
// readTeams reads in the list of team IDs,
|
||||
// so we can quickly validate them.
|
||||
func (ctx *Instance) readTeams() {
|
||||
filepath := ctx.StatePath("teamids.txt")
|
||||
teamids, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
log.Printf("Error openining %s: %s", filepath, err)
|
||||
return
|
||||
}
|
||||
defer teamids.Close()
|
||||
|
||||
// List out team IDs
|
||||
newList := map[string]bool{}
|
||||
scanner := bufio.NewScanner(teamids)
|
||||
for scanner.Scan() {
|
||||
teamId := scanner.Text()
|
||||
if (teamId == "..") || strings.ContainsAny(teamId, "/") {
|
||||
log.Printf("Dangerous team ID dropped: %s", teamId)
|
||||
continue
|
||||
}
|
||||
newList[scanner.Text()] = true
|
||||
}
|
||||
teamList := ctx.State.getTeams()
|
||||
|
||||
// For any new team IDs, set their next attempt time to right now
|
||||
now := time.Now()
|
||||
added := 0
|
||||
for k, _ := range newList {
|
||||
for teamName, _ := range teamList {
|
||||
ctx.nextAttemptMutex.RLock()
|
||||
_, ok := ctx.nextAttempt[k]
|
||||
_, ok := ctx.nextAttempt[teamName]
|
||||
ctx.nextAttemptMutex.RUnlock()
|
||||
|
||||
if !ok {
|
||||
ctx.nextAttemptMutex.Lock()
|
||||
ctx.nextAttempt[k] = now
|
||||
ctx.nextAttempt[teamName] = now
|
||||
ctx.nextAttemptMutex.Unlock()
|
||||
|
||||
added += 1
|
||||
|
@ -188,9 +168,9 @@ func (ctx *Instance) readTeams() {
|
|||
// For any removed team IDs, remove them
|
||||
removed := 0
|
||||
ctx.nextAttemptMutex.Lock() // XXX: This could be less of a cludgel
|
||||
for k, _ := range ctx.nextAttempt {
|
||||
if _, ok := newList[k]; !ok {
|
||||
delete(ctx.nextAttempt, k)
|
||||
for teamName, _ := range ctx.nextAttempt {
|
||||
if _, ok := teamList[teamName]; !ok {
|
||||
delete(ctx.nextAttempt, teamName)
|
||||
}
|
||||
}
|
||||
ctx.nextAttemptMutex.Unlock()
|
||||
|
@ -200,81 +180,9 @@ func (ctx *Instance) readTeams() {
|
|||
}
|
||||
}
|
||||
|
||||
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
|
||||
// removing each points.new/ file as it goes.
|
||||
func (ctx *Instance) collectPoints() {
|
||||
logf, err := os.OpenFile(ctx.StatePath("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Printf("Can't append to points log: %s", err)
|
||||
return
|
||||
}
|
||||
defer logf.Close()
|
||||
|
||||
files, err := ioutil.ReadDir(ctx.StatePath("points.new"))
|
||||
if err != nil {
|
||||
log.Printf("Error reading packages: %s", err)
|
||||
}
|
||||
for _, f := range files {
|
||||
filename := ctx.StatePath("points.new", f.Name())
|
||||
s, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Printf("Can't read points file %s: %s", filename, err)
|
||||
continue
|
||||
}
|
||||
award, err := ParseAward(string(s))
|
||||
if err != nil {
|
||||
log.Printf("Can't parse award file %s: %s", filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
duplicate := false
|
||||
for _, e := range ctx.PointsLog("") {
|
||||
if award.Same(e) {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if duplicate {
|
||||
log.Printf("Skipping duplicate points: %s", award.String())
|
||||
} else {
|
||||
fmt.Fprintf(logf, "%s\n", award.String())
|
||||
}
|
||||
|
||||
logf.Sync()
|
||||
if err := os.Remove(filename); err != nil {
|
||||
log.Printf("Unable to remove %s: %s", filename, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *Instance) isEnabled() bool {
|
||||
// Skip if we've been disabled
|
||||
if _, err := os.Stat(ctx.StatePath("disabled")); err == nil {
|
||||
log.Print("Suspended: disabled file found")
|
||||
return false
|
||||
}
|
||||
|
||||
untilspec, err := ioutil.ReadFile(ctx.StatePath("until"))
|
||||
if err == nil {
|
||||
untilspecs := strings.TrimSpace(string(untilspec))
|
||||
until, err := time.Parse(time.RFC3339, untilspecs)
|
||||
if err != nil {
|
||||
log.Printf("Suspended: Unparseable until date: %s", untilspec)
|
||||
return false
|
||||
}
|
||||
if until.Before(time.Now()) {
|
||||
log.Print("Suspended: until time reached, suspending maintenance")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (ctx *Instance) UpdateConfig() {
|
||||
// Handle export manifest
|
||||
if _, err := os.Stat(ctx.StatePath("export_manifest")); err == nil {
|
||||
if _, err := ctx.State.getConfig("export_manifest"); err == nil {
|
||||
if !ctx.Runtime.export_manifest {
|
||||
log.Print("Enabling manifest export")
|
||||
ctx.Runtime.export_manifest = true
|
||||
|
@ -289,10 +197,9 @@ func (ctx *Instance) UpdateConfig() {
|
|||
// maintenance is the goroutine that runs a periodic maintenance task
|
||||
func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
|
||||
for {
|
||||
if ctx.isEnabled() {
|
||||
if ctx.State.isEnabled() {
|
||||
ctx.tidy()
|
||||
ctx.readTeams()
|
||||
ctx.collectPoints()
|
||||
ctx.generatePuzzleList()
|
||||
ctx.generatePointsLog("")
|
||||
}
|
||||
|
|
24
src/mothd.go
24
src/mothd.go
|
@ -17,6 +17,10 @@ func setup() error {
|
|||
func main() {
|
||||
ctx := &Instance{}
|
||||
|
||||
var state_path string
|
||||
var state_engine_choice string
|
||||
var state_engine MOTHState
|
||||
|
||||
flag.StringVar(
|
||||
&ctx.Base,
|
||||
"base",
|
||||
|
@ -30,7 +34,13 @@ func main() {
|
|||
"Path to read mothballs",
|
||||
)
|
||||
flag.StringVar(
|
||||
&ctx.StateDir,
|
||||
&state_engine_choice,
|
||||
"state-engine",
|
||||
"legacy",
|
||||
"State engine to use (default: legacy, alt: sqlite)",
|
||||
)
|
||||
flag.StringVar(
|
||||
&state_path,
|
||||
"state",
|
||||
"/state",
|
||||
"Path to write state",
|
||||
|
@ -59,10 +69,22 @@ func main() {
|
|||
)
|
||||
flag.Parse()
|
||||
|
||||
|
||||
if (state_engine_choice == "legacy") {
|
||||
lm_engine := &LegacyMOTHState{}
|
||||
lm_engine.StateDir = state_path
|
||||
lm_engine.maintenanceInterval = *maintenanceInterval
|
||||
state_engine = lm_engine
|
||||
} else {
|
||||
log.Fatal("Unrecognized state engine '", state_engine_choice, "'")
|
||||
}
|
||||
|
||||
if err := setup(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ctx.State = state_engine
|
||||
|
||||
err := ctx.Initialize()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type MOTHState interface {
|
||||
PointsLog(teamId string) []*Award
|
||||
AwardPoints(teamID string, category string, points int) error
|
||||
|
||||
TeamName(teamId string) (string, error)
|
||||
isEnabled() bool
|
||||
getConfig(configName string) (string, error)
|
||||
getTeams() map[string]struct{}
|
||||
login(teamId string, token string) (bool, error)
|
||||
Initialize() (bool, error)
|
||||
}
|
||||
|
||||
var ErrAlreadyRegistered = errors.New("This team ID has already been registered")
|
||||
var ErrInvalidTeamID = errors.New("Invalid team ID")
|
||||
var ErrRegistrationError = errors.New("Unable to register. Perhaps a teammate has already registered")
|
Loading…
Reference in New Issue