moth/src/instance.go

281 lines
6.6 KiB
Go
Raw Normal View History

2018-09-14 18:24:48 -06:00
package main
import (
"bufio"
"fmt"
"io"
2018-09-14 18:24:48 -06:00
"io/ioutil"
2018-09-17 17:00:08 -06:00
"log"
2019-02-25 09:07:53 -07:00
"math/rand"
"net/http"
2018-09-17 17:00:08 -06:00
"os"
2018-09-14 18:24:48 -06:00
"path"
"strings"
"sync"
2018-09-17 17:00:08 -06:00
"time"
2018-09-14 18:24:48 -06:00
)
type RuntimeConfig struct {
export_manifest bool
}
2018-09-14 18:24:48 -06:00
type Instance struct {
Base string
MothballDir string
StateDir string
ThemeDir string
AttemptInterval time.Duration
2020-11-11 13:13:09 -07:00
UseXForwarded bool
2019-11-13 13:47:56 -07:00
Runtime RuntimeConfig
categories map[string]*Mothball
MaxPointsUnlocked map[string]int
update chan bool
jPuzzleList []byte
jPointsLog []byte
2020-11-11 13:13:09 -07:00
eventStream chan string
2019-11-13 13:47:56 -07:00
nextAttempt map[string]time.Time
nextAttemptMutex *sync.RWMutex
mux *http.ServeMux
2018-09-14 18:24:48 -06:00
}
func (ctx *Instance) Initialize() error {
2018-09-14 18:24:48 -06:00
// Roll over and die if directories aren't even set up
if _, err := os.Stat(ctx.MothballDir); err != nil {
return err
2018-09-14 18:24:48 -06:00
}
if _, err := os.Stat(ctx.StateDir); err != nil {
return err
2018-09-14 18:24:48 -06:00
}
2019-02-25 09:07:53 -07:00
ctx.Base = strings.TrimRight(ctx.Base, "/")
ctx.categories = map[string]*Mothball{}
ctx.update = make(chan bool, 10)
2020-11-11 13:13:09 -07:00
ctx.eventStream = make(chan string, 80)
ctx.nextAttempt = map[string]time.Time{}
ctx.nextAttemptMutex = new(sync.RWMutex)
ctx.mux = http.NewServeMux()
ctx.BindHandlers()
2018-09-17 17:00:08 -06:00
ctx.MaybeInitialize()
return nil
2018-09-14 18:24:48 -06:00
}
2019-02-21 19:38:53 -07:00
// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift
2019-02-21 19:40:48 -07:00
const distinguishableChars = "234678abcdefhijkmnpqrtwxyz="
2019-02-21 19:38:53 -07:00
func mktoken() string {
a := make([]byte, 8)
2019-02-25 09:07:53 -07:00
for i := range a {
2019-02-21 19:38:53 -07:00
char := rand.Intn(len(distinguishableChars))
a[i] = distinguishableChars[char]
}
return string(a)
}
2018-09-17 17:00:08 -06:00
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"))
2020-11-11 13:13:09 -07:00
os.Remove(ctx.StatePath("events.log"))
2018-09-17 17:00:08 -06:00
os.RemoveAll(ctx.StatePath("points.tmp"))
os.RemoveAll(ctx.StatePath("points.new"))
os.RemoveAll(ctx.StatePath("teams"))
// Make sure various subdirectories exist
2018-09-14 18:24:48 -06:00
os.Mkdir(ctx.StatePath("points.tmp"), 0755)
os.Mkdir(ctx.StatePath("points.new"), 0755)
2018-09-17 17:00:08 -06:00
os.Mkdir(ctx.StatePath("teams"), 0755)
2018-09-14 18:24:48 -06:00
// Preseed available team ids if file doesn't exist
2018-09-17 17:00:08 -06:00
if f, err := os.OpenFile(ctx.StatePath("teamids.txt"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil {
2018-09-14 18:24:48 -06:00
defer f.Close()
2019-02-21 19:38:53 -07:00
for i := 0; i <= 100; i += 1 {
fmt.Fprintln(f, mktoken())
2018-09-14 18:24:48 -06:00
}
}
2018-09-17 17:00:08 -06:00
2020-11-11 13:13:09 -07:00
// Record that we did all this
ctx.LogEvent("init", "", "", "", 0)
2018-09-17 17:00:08 -06:00
// 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)
2018-09-14 18:24:48 -06:00
}
2018-09-17 17:00:08 -06:00
defer f.Close()
fmt.Fprintln(f, "Remove this file to reinitialize the contest")
2018-09-14 18:24:48 -06:00
}
2020-11-11 13:13:09 -07:00
func logstr(s string) string {
if s == "" {
return "-"
}
return s
}
// LogEvent writes to the event log
func (ctx *Instance) LogEvent(event, participantID, teamID, cat string, points int, extra ...string) {
event = strings.ReplaceAll(event, " ", "-")
msg := fmt.Sprintf(
2020-11-13 18:42:23 -07:00
"%d %s %s %s %s %d",
time.Now().Unix(),
2020-11-11 13:13:09 -07:00
logstr(event),
logstr(participantID),
logstr(teamID),
logstr(cat),
points,
)
for _, x := range extra {
msg = msg + " " + strings.ReplaceAll(x, " ", "-")
}
ctx.eventStream <- msg
}
func pathCleanse(parts []string) string {
clean := make([]string, len(parts))
for i := range parts {
part := parts[i]
part = strings.TrimLeft(part, ".")
if p := strings.LastIndex(part, "/"); p >= 0 {
part = part[p+1:]
}
clean[i] = part
}
return path.Join(clean...)
}
2018-09-14 18:24:48 -06:00
func (ctx Instance) MothballPath(parts ...string) string {
tail := pathCleanse(parts)
2018-09-14 18:24:48 -06:00
return path.Join(ctx.MothballDir, tail)
}
func (ctx *Instance) StatePath(parts ...string) string {
tail := pathCleanse(parts)
2018-09-14 18:24:48 -06:00
return path.Join(ctx.StateDir, tail)
}
func (ctx *Instance) ThemePath(parts ...string) string {
tail := pathCleanse(parts)
return path.Join(ctx.ThemeDir, tail)
}
func (ctx *Instance) TooFast(teamId string) bool {
now := time.Now()
2019-11-13 13:47:56 -07:00
ctx.nextAttemptMutex.RLock()
next, _ := ctx.nextAttempt[teamId]
ctx.nextAttemptMutex.RUnlock()
2019-11-13 13:47:56 -07:00
ctx.nextAttemptMutex.Lock()
ctx.nextAttempt[teamId] = now.Add(ctx.AttemptInterval)
ctx.nextAttemptMutex.Unlock()
2019-11-13 13:47:56 -07:00
return now.Before(next)
}
func (ctx *Instance) PointsLog(teamId string) AwardList {
awardlist := AwardList{}
2020-11-11 13:13:09 -07:00
2018-09-14 18:24:48 -06:00
fn := ctx.StatePath("points.log")
2018-09-14 18:24:48 -06:00
f, err := os.Open(fn)
if err != nil {
log.Printf("Unable to open %s: %s", fn, err)
return awardlist
2018-09-14 18:24:48 -06:00
}
defer f.Close()
2018-09-17 17:00:08 -06:00
2018-09-14 18:24:48 -06:00
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
}
awardlist = append(awardlist, cur)
2018-09-14 18:24:48 -06:00
}
2018-09-17 17:00:08 -06:00
return awardlist
2018-09-14 18:24:48 -06:00
}
// 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 {
2018-09-17 18:02:44 -06:00
a := Award{
2018-09-17 21:32:24 -06:00
When: time.Now(),
TeamId: teamId,
2018-09-17 18:02:44 -06:00
Category: category,
2018-09-17 21:32:24 -06:00
Points: points,
2018-09-17 18:02:44 -06:00
}
_, err := ctx.TeamName(teamId)
if err != nil {
return fmt.Errorf("No registered team with this hash")
}
for _, e := range ctx.PointsLog("") {
2018-09-17 18:02:44 -06:00
if a.Same(e) {
return fmt.Errorf("Points already awarded to this team in this category")
}
}
2018-09-17 21:32:24 -06:00
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
}
2018-09-17 18:02:44 -06:00
if err := os.Rename(tmpfn, newfn); err != nil {
return err
}
2018-09-19 17:56:26 -06:00
ctx.update <- true
log.Printf("Award %s %s %d", teamId, category, points)
return nil
2018-09-17 18:02:44 -06:00
}
func (ctx *Instance) OpenCategoryFile(category string, parts ...string) (io.ReadCloser, error) {
mb, ok := ctx.categories[category]
if !ok {
return nil, fmt.Errorf("No such category: %s", category)
}
filename := path.Join(parts...)
f, err := mb.Open(filename)
return f, err
}
2018-09-18 21:29:05 -06:00
func (ctx *Instance) ValidTeamId(teamId string) bool {
ctx.nextAttemptMutex.RLock()
_, ok := ctx.nextAttempt[teamId]
ctx.nextAttemptMutex.RUnlock()
return ok
}
2018-09-18 21:29:05 -06:00
func (ctx *Instance) TeamName(teamId string) (string, error) {
teamNameBytes, err := ioutil.ReadFile(ctx.StatePath("teams", teamId))
teamName := strings.TrimSpace(string(teamNameBytes))
return teamName, err
}