moth/src/maintenance.go

338 lines
8.0 KiB
Go
Raw Normal View History

2018-09-14 18:24:48 -06:00
package main
import (
2018-09-19 21:44:34 -06:00
"bufio"
"encoding/json"
2018-09-17 17:00:08 -06:00
"fmt"
2018-09-14 18:24:48 -06:00
"io/ioutil"
2018-09-17 17:00:08 -06:00
"log"
2018-09-14 18:24:48 -06:00
"os"
"sort"
2018-09-19 21:44:34 -06:00
"strconv"
2018-09-14 18:24:48 -06:00
"strings"
2018-09-17 17:00:08 -06:00
"time"
2018-09-14 18:24:48 -06:00
)
2018-09-19 21:44:34 -06:00
func (pm *PuzzleMap) MarshalJSON() ([]byte, error) {
if pm == nil {
return []byte("null"), nil
}
jPath, err := json.Marshal(pm.Path)
if err != nil {
return nil, err
}
ret := fmt.Sprintf("[%d,%s]", pm.Points, string(jPath))
return []byte(ret), nil
}
2018-10-02 19:21:54 -06:00
func (ctx *Instance) generatePuzzleList() {
2018-09-19 21:44:34 -06:00
maxByCategory := map[string]int{}
for _, a := range ctx.PointsLog("") {
2018-09-19 21:44:34 -06:00
if a.Points > maxByCategory[a.Category] {
maxByCategory[a.Category] = a.Points
}
}
ret := map[string][]PuzzleMap{}
for catName, mb := range ctx.categories {
filtered_puzzlemap := make([]PuzzleMap, 0, 30)
2018-09-19 21:44:34 -06:00
completed := true
for _, pm := range mb.puzzlemap {
filtered_puzzlemap = append(filtered_puzzlemap, pm)
2018-09-19 21:44:34 -06:00
if pm.Points > maxByCategory[catName] {
2018-09-19 21:44:34 -06:00
completed = false
maxByCategory[catName] = pm.Points
2018-09-19 21:44:34 -06:00
break
}
}
2018-09-19 21:44:34 -06:00
if completed {
filtered_puzzlemap = append(filtered_puzzlemap, PuzzleMap{0, ""})
2018-09-19 21:44:34 -06:00
}
ret[catName] = filtered_puzzlemap
2018-09-19 21:44:34 -06:00
}
// Cache the unlocked points for use in other functions
ctx.MaxPointsUnlocked = maxByCategory
2018-09-19 21:44:34 -06:00
jpl, err := json.Marshal(ret)
2018-10-02 19:21:54 -06:00
if err != nil {
log.Printf("Marshalling puzzles.js: %v", err)
return
2018-09-19 21:44:34 -06:00
}
2018-10-02 19:21:54 -06:00
ctx.jPuzzleList = jpl
2018-09-19 21:44:34 -06:00
}
func (ctx *Instance) generatePointsLog(teamId string) []byte {
2018-09-19 21:44:34 -06:00
var ret struct {
Teams map[string]string `json:"teams"`
Points []*Award `json:"points"`
}
ret.Teams = map[string]string{}
ret.Points = ctx.PointsLog(teamId)
2018-09-19 21:44:34 -06:00
teamNumbersById := map[string]int{}
for nr, a := range ret.Points {
teamNumber, ok := teamNumbersById[a.TeamId]
if !ok {
teamName, err := ctx.TeamName(a.TeamId)
if err != nil {
2018-10-02 19:21:54 -06:00
teamName = "Rodney" // https://en.wikipedia.org/wiki/Rogue_(video_game)#Gameplay
2018-09-19 21:44:34 -06:00
}
teamNumber = nr
teamNumbersById[a.TeamId] = teamNumber
ret.Teams[strconv.FormatInt(int64(teamNumber), 16)] = teamName
}
a.TeamId = strconv.FormatInt(int64(teamNumber), 16)
}
jpl, err := json.Marshal(ret)
2018-10-02 19:21:54 -06:00
if err != nil {
log.Printf("Marshalling points.js: %v", err)
return nil
}
if len(teamId) == 0 {
ctx.jPointsLog = jpl
2018-09-19 21:44:34 -06:00
}
return jpl
2018-09-19 21:44:34 -06:00
}
2018-09-14 18:24:48 -06:00
// maintenance runs
2018-09-19 21:44:34 -06:00
func (ctx *Instance) tidy() {
2018-09-17 17:00:08 -06:00
// Do they want to reset everything?
ctx.MaybeInitialize()
// Check set config
ctx.UpdateConfig()
2018-09-18 21:29:05 -06:00
// Refresh all current categories
for categoryName, mb := range ctx.categories {
2018-09-18 21:29:05 -06:00
if err := mb.Refresh(); err != nil {
// Backing file vanished: remove this category
log.Printf("Removing category: %s: %s", categoryName, err)
mb.Close()
delete(ctx.categories, categoryName)
2018-09-18 21:29:05 -06:00
}
}
2018-09-14 18:24:48 -06:00
// Any new categories?
files, err := ioutil.ReadDir(ctx.MothballPath())
if err != nil {
log.Printf("Error listing mothballs: %s", err)
}
for _, f := range files {
filename := f.Name()
filepath := ctx.MothballPath(filename)
2018-09-17 17:00:08 -06:00
if !strings.HasSuffix(filename, ".mb") {
2018-09-14 18:24:48 -06:00
continue
}
categoryName := strings.TrimSuffix(filename, ".mb")
2018-09-17 17:00:08 -06:00
if _, ok := ctx.categories[categoryName]; !ok {
2018-09-14 18:24:48 -06:00
mb, err := OpenMothball(filepath)
if err != nil {
log.Printf("Error opening %s: %s", filepath, err)
continue
}
2018-09-17 17:00:08 -06:00
log.Printf("New category: %s", filename)
ctx.categories[categoryName] = mb
2018-09-14 18:24:48 -06:00
}
}
}
// 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
}
// For any new team IDs, set their next attempt time to right now
now := time.Now()
added := 0
for k, _ := range newList {
ctx.nextAttemptMutex.RLock()
_, ok := ctx.nextAttempt[k]
ctx.nextAttemptMutex.RUnlock()
if !ok {
ctx.nextAttemptMutex.Lock()
ctx.nextAttempt[k] = now
ctx.nextAttemptMutex.Unlock()
added += 1
}
}
// 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)
}
}
ctx.nextAttemptMutex.Unlock()
if (added > 0) || (removed > 0) {
log.Printf("Team IDs updated: %d added, %d removed", added, removed)
}
}
2018-09-14 18:24:48 -06:00
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
// removing each points.new/ file as it goes.
2018-09-19 21:44:34 -06:00
func (ctx *Instance) collectPoints() {
2020-11-11 13:13:09 -07:00
points := ctx.PointsLog("")
pointsFilename := ctx.StatePath("points.log")
pointsNewFilename := ctx.StatePath("points.log.new")
// Yo, this is delicate.
// If we have to return early, we must remove this file.
// If the file's written and we move it successfully,
// we need to remove all the little points files that built it.
newPoints, err := os.OpenFile(pointsNewFilename, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
2018-09-14 18:24:48 -06:00
if err != nil {
log.Printf("Can't append to points log: %s", err)
return
}
2018-09-17 17:00:08 -06:00
2018-09-14 18:24:48 -06:00
files, err := ioutil.ReadDir(ctx.StatePath("points.new"))
if err != nil {
log.Printf("Error reading packages: %s", err)
}
removearino := make([]string, 0, len(files))
2018-09-14 18:24:48 -06:00
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
}
2018-09-17 21:32:24 -06:00
2018-09-17 18:02:44 -06:00
duplicate := false
for _, e := range points {
2018-09-17 18:02:44 -06:00
if award.Same(e) {
duplicate = true
break
}
}
2018-09-17 21:32:24 -06:00
2018-09-17 18:02:44 -06:00
if duplicate {
log.Printf("Skipping duplicate points: %s", award.String())
} else {
2020-11-11 13:13:09 -07:00
points = append(points, award)
2018-09-14 18:24:48 -06:00
}
removearino = append(removearino, filename)
2018-09-14 18:24:48 -06:00
}
sort.Stable(points)
for _, point := range points {
fmt.Fprintln(newPoints, point.String())
}
2020-11-11 13:13:09 -07:00
newPoints.Close()
2020-11-11 13:13:09 -07:00
if err := os.Rename(pointsNewFilename, pointsFilename); err != nil {
2020-11-11 13:13:09 -07:00
log.Printf("Unable to move %s to %s: %s", pointsFilename, pointsNewFilename, err)
if err := os.Remove(pointsNewFilename); err != nil {
log.Printf("Also couldn't remove %s: %s", pointsNewFilename, err)
}
return
}
for _, filename := range removearino {
2020-11-11 13:13:09 -07:00
if err := os.Remove(filename); err != nil {
log.Printf("Unable to remove %s: %s", filename, err)
}
}
}
2018-09-21 17:45:28 -06:00
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
}
2018-09-21 17:45:28 -06:00
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 {
2019-11-13 13:47:56 -07:00
if !ctx.Runtime.export_manifest {
log.Print("Enabling manifest export")
ctx.Runtime.export_manifest = true
}
2019-11-13 13:47:56 -07:00
} else if ctx.Runtime.export_manifest {
log.Print("Disabling manifest export")
ctx.Runtime.export_manifest = false
}
}
2018-09-14 18:24:48 -06:00
// maintenance is the goroutine that runs a periodic maintenance task
func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
2018-09-19 17:56:26 -06:00
for {
2018-09-21 17:45:28 -06:00
if ctx.isEnabled() {
2018-09-20 10:15:34 -06:00
ctx.tidy()
ctx.readTeams()
2018-09-20 10:15:34 -06:00
ctx.collectPoints()
ctx.generatePuzzleList()
ctx.generatePointsLog("")
2020-11-11 13:13:09 -07:00
} else {
ctx.LogEvent("disabled", "", "", "", 0)
2018-09-20 10:15:34 -06:00
}
2018-09-19 17:56:26 -06:00
select {
2018-09-19 21:44:34 -06:00
case <-ctx.update:
// log.Print("Forced update")
2020-11-11 13:13:09 -07:00
case msg := <-ctx.eventStream:
fmt.Fprintln(ctx.eventLogWriter, msg)
2018-09-19 21:44:34 -06:00
case <-time.After(maintenanceInterval):
// log.Print("Housekeeping...")
2018-09-19 17:56:26 -06:00
}
2018-09-14 18:24:48 -06:00
}
}