mirror of https://github.com/dirtbags/moth.git
Refactor state functions into their own doodad
This commit is contained in:
parent
c6f8e87bb6
commit
657a02e773
|
@ -1,14 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Award struct {
|
||||
When time.Time
|
||||
// Unix epoch time of this event
|
||||
When int64
|
||||
TeamId string
|
||||
Category string
|
||||
Points int
|
||||
|
@ -19,44 +18,18 @@ func ParseAward(s string) (*Award, error) {
|
|||
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
var whenEpoch int64
|
||||
|
||||
n, err := fmt.Sscanf(s, "%d %s %s %d", &whenEpoch, &ret.TeamId, &ret.Category, &ret.Points)
|
||||
n, err := fmt.Sscanf(s, "%d %s %s %d", &ret.When, &ret.TeamId, &ret.Category, &ret.Points)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if n != 4 {
|
||||
return nil, fmt.Errorf("Malformed award string: only parsed %d fields", n)
|
||||
}
|
||||
|
||||
ret.When = time.Unix(whenEpoch, 0)
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (a *Award) String() string {
|
||||
return fmt.Sprintf("%d %s %s %d", a.When.Unix(), a.TeamId, a.Category, a.Points)
|
||||
}
|
||||
|
||||
func (a *Award) MarshalJSON() ([]byte, error) {
|
||||
if a == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
jTeamId, err := json.Marshal(a.TeamId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jCategory, err := json.Marshal(a.Category)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := fmt.Sprintf(
|
||||
"[%d,%s,%s,%d]",
|
||||
a.When.Unix(),
|
||||
jTeamId,
|
||||
jCategory,
|
||||
a.Points,
|
||||
)
|
||||
return []byte(ret), nil
|
||||
return fmt.Sprintf("%d %s %s %d", a.When, a.TeamId, a.Category, a.Points)
|
||||
}
|
||||
|
||||
func (a *Award) Same(o *Award) bool {
|
||||
|
|
|
@ -0,0 +1,356 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
"bufio"
|
||||
"time"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift
|
||||
const distinguishableChars = "234678abcdefhijkmnpqrtwxyz="
|
||||
|
||||
func mktoken() string {
|
||||
a := make([]byte, 8)
|
||||
for i := range a {
|
||||
char := rand.Intn(len(distinguishableChars))
|
||||
a[i] = distinguishableChars[char]
|
||||
}
|
||||
return string(a)
|
||||
}
|
||||
|
||||
type StateExport struct {
|
||||
TeamNames map[string]string
|
||||
PointsLog []Award
|
||||
Messages []string
|
||||
}
|
||||
|
||||
// We use the filesystem for synchronization between threads.
|
||||
// The only thing State methods need to know is the path to the state directory.
|
||||
type State struct {
|
||||
StateDir string
|
||||
update chan bool
|
||||
}
|
||||
|
||||
func NewState(stateDir string) (*State) {
|
||||
return &State{
|
||||
StateDir: stateDir,
|
||||
update: make(chan bool, 10),
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a cleaned up join of path parts relative to
|
||||
func (s *State) path(parts ...string) string {
|
||||
rel := filepath.Clean(filepath.Join(parts...))
|
||||
parts = filepath.SplitList(rel)
|
||||
for i, part := range parts {
|
||||
part = strings.TrimLeft(part, "./\\:")
|
||||
parts[i] = part
|
||||
}
|
||||
rel = filepath.Join(parts...)
|
||||
return filepath.Join(s.StateDir, rel)
|
||||
}
|
||||
|
||||
// Check a few things to see if this state directory is "enabled".
|
||||
func (s *State) Enabled() bool {
|
||||
if _, err := os.Stat(s.path("enabled")); os.IsNotExist(err) {
|
||||
log.Print("Suspended: enabled file missing")
|
||||
return false
|
||||
}
|
||||
|
||||
untilspec, err := ioutil.ReadFile(s.path("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
|
||||
}
|
||||
|
||||
// Returns team name given a team ID.
|
||||
func (s *State) TeamName(teamId string) (string, error) {
|
||||
teamFile := s.path("teams", teamId)
|
||||
teamNameBytes, err := ioutil.ReadFile(teamFile)
|
||||
teamName := strings.TrimSpace(string(teamNameBytes))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return teamName, nil
|
||||
}
|
||||
|
||||
// Write out team name. This can only be done once.
|
||||
func (s *State) SetTeamName(teamId string, teamName string) error {
|
||||
teamFile := s.path("teams", teamId)
|
||||
err := ioutil.WriteFile(teamFile, []byte(teamName), os.ModeExclusive | 0644)
|
||||
return err
|
||||
}
|
||||
|
||||
// Retrieve the current points log
|
||||
func (s *State) PointsLog() ([]*Award) {
|
||||
pointsFile := s.path("points.log")
|
||||
f, err := os.Open(pointsFile)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
pointsLog := make([]*Award, 0, 200)
|
||||
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
|
||||
}
|
||||
pointsLog = append(pointsLog, cur)
|
||||
}
|
||||
return pointsLog
|
||||
}
|
||||
|
||||
// Return an exportable points log,
|
||||
// This anonymizes teamId with either an integer, or the string "self"
|
||||
// for the requesting teamId.
|
||||
func (s *State) Export(teamId string) (*StateExport) {
|
||||
teamName, _ := s.TeamName(teamId)
|
||||
|
||||
pointsLog := s.PointsLog()
|
||||
|
||||
export := StateExport{
|
||||
PointsLog: make([]Award, len(pointsLog)),
|
||||
Messages: make([]string, 0, 10),
|
||||
TeamNames: map[string]string{"self": teamName},
|
||||
}
|
||||
|
||||
// Read in messages
|
||||
messagesFile := s.path("messages.txt")
|
||||
if f, err := os.Open(messagesFile); err != nil {
|
||||
log.Print(err)
|
||||
} else {
|
||||
defer f.Close()
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
message := scanner.Text()
|
||||
if strings.HasPrefix(message, "#") {
|
||||
continue
|
||||
}
|
||||
export.Messages = append(export.Messages, message)
|
||||
}
|
||||
}
|
||||
|
||||
// Read in points
|
||||
exportIds := map[string]string{teamId: "self"}
|
||||
for logno, award := range pointsLog {
|
||||
exportAward := award
|
||||
if id, ok := exportIds[award.TeamId]; ok {
|
||||
exportAward.TeamId = id
|
||||
} else {
|
||||
exportId := strconv.Itoa(logno)
|
||||
exportAward.TeamId = exportId
|
||||
exportIds[award.TeamId] = exportAward.TeamId
|
||||
|
||||
name, err := s.TeamName(award.TeamId)
|
||||
if err != nil {
|
||||
name = "Rodney" // https://en.wikipedia.org/wiki/Rogue_(video_game)#Gameplay
|
||||
}
|
||||
export.TeamNames[exportId] = name
|
||||
}
|
||||
export.PointsLog[logno] = *exportAward
|
||||
}
|
||||
|
||||
return &export
|
||||
}
|
||||
|
||||
// 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 update task makes sure we never have duplicate points in the log.
|
||||
func (s *State) AwardPoints(teamId, category string, points int) error {
|
||||
a := Award{
|
||||
When: time.Now().Unix(),
|
||||
TeamId: teamId,
|
||||
Category: category,
|
||||
Points: points,
|
||||
}
|
||||
|
||||
_, err := s.TeamName(teamId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, e := range s.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 := s.path("points.tmp", fn)
|
||||
newfn := s.path("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
|
||||
}
|
||||
|
||||
s.update <- true
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
|
||||
// removing each points.new/ file as it goes.
|
||||
func (s *State) collectPoints() {
|
||||
files, err := ioutil.ReadDir(s.path("points.new"))
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
for _, f := range files {
|
||||
filename := s.path("points.new", f.Name())
|
||||
awardstr, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Print("Opening new points: ", err)
|
||||
continue
|
||||
}
|
||||
award, err := ParseAward(string(awardstr))
|
||||
if err != nil {
|
||||
log.Print("Can't parse award file ", filename, ": ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
duplicate := false
|
||||
for _, e := range s.PointsLog() {
|
||||
if award.Same(e) {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if duplicate {
|
||||
log.Print("Skipping duplicate points: ", award.String())
|
||||
} else {
|
||||
log.Print("Award: ", award.String())
|
||||
|
||||
logf, err := os.OpenFile(s.path("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Print("Can't append to points log: ", err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(logf, award.String())
|
||||
logf.Close()
|
||||
}
|
||||
|
||||
if err := os.Remove(filename); err != nil {
|
||||
log.Print("Unable to remove new points file: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (s *State) maybeInitialize() {
|
||||
// Are we supposed to re-initialize?
|
||||
if _, err := os.Stat(s.path("initialized")); ! os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Print("initialized file missing, re-initializing")
|
||||
|
||||
// Remove any extant control and state files
|
||||
os.Remove(s.path("enabled"))
|
||||
os.Remove(s.path("until"))
|
||||
os.Remove(s.path("points.log"))
|
||||
os.Remove(s.path("messages.txt"))
|
||||
os.RemoveAll(s.path("points.tmp"))
|
||||
os.RemoveAll(s.path("points.new"))
|
||||
os.RemoveAll(s.path("teams"))
|
||||
|
||||
// Make sure various subdirectories exist
|
||||
os.Mkdir(s.path("points.tmp"), 0755)
|
||||
os.Mkdir(s.path("points.new"), 0755)
|
||||
os.Mkdir(s.path("teams"), 0755)
|
||||
|
||||
// Preseed available team ids if file doesn't exist
|
||||
if f, err := os.OpenFile(s.path("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 some files
|
||||
ioutil.WriteFile(
|
||||
s.path("initialized"),
|
||||
[]byte("Remove this file to re-initialized the contest\n"),
|
||||
0644,
|
||||
)
|
||||
ioutil.WriteFile(
|
||||
s.path("enabled"),
|
||||
[]byte("Remove this file to suspend the contest\n"),
|
||||
0644,
|
||||
)
|
||||
ioutil.WriteFile(
|
||||
s.path("until"),
|
||||
[]byte("3009-10-31T00:00:00Z\n"),
|
||||
0644,
|
||||
)
|
||||
ioutil.WriteFile(
|
||||
s.path("messages.txt"),
|
||||
[]byte(fmt.Sprintf("[%s] Initialized.\n", time.Now())),
|
||||
0644,
|
||||
)
|
||||
ioutil.WriteFile(
|
||||
s.path("points.log"),
|
||||
[]byte(""),
|
||||
0644,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *State) Run(updateInterval time.Duration) {
|
||||
for {
|
||||
s.maybeInitialize()
|
||||
if s.Enabled() {
|
||||
s.collectPoints()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.update:
|
||||
case <-time.After(updateInterval):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
s := NewState("./state")
|
||||
go s.Run(2 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
|
||||
fmt.Println(s.Export(""))
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Loading…
Reference in New Issue