2018-09-14 18:24:48 -06:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"fmt"
|
2018-09-17 17:40:05 -06:00
|
|
|
"io"
|
2018-09-14 18:24:48 -06:00
|
|
|
"io/ioutil"
|
2018-09-17 17:00:08 -06:00
|
|
|
"log"
|
|
|
|
"os"
|
2018-09-14 18:24:48 -06:00
|
|
|
"path"
|
|
|
|
"strings"
|
2018-09-17 17:00:08 -06:00
|
|
|
"time"
|
2018-09-14 18:24:48 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
type Instance struct {
|
2018-09-17 17:00:08 -06:00
|
|
|
Base string
|
|
|
|
MothballDir string
|
|
|
|
StateDir string
|
|
|
|
ResourcesDir string
|
|
|
|
Categories map[string]*Mothball
|
2018-09-19 17:56:26 -06:00
|
|
|
update chan bool
|
2018-09-14 18:24:48 -06:00
|
|
|
}
|
|
|
|
|
2018-09-17 17:00:08 -06:00
|
|
|
func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, error) {
|
2018-09-14 18:24:48 -06:00
|
|
|
ctx := &Instance{
|
2018-09-17 17:00:08 -06:00
|
|
|
Base: strings.TrimRight(base, "/"),
|
|
|
|
MothballDir: mothballDir,
|
|
|
|
StateDir: stateDir,
|
|
|
|
ResourcesDir: resourcesDir,
|
|
|
|
Categories: map[string]*Mothball{},
|
2018-09-19 17:56:26 -06:00
|
|
|
update: make(chan bool, 10),
|
2018-09-14 18:24:48 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// Roll over and die if directories aren't even set up
|
|
|
|
if _, err := os.Stat(mothballDir); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if _, err := os.Stat(stateDir); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-09-17 17:00:08 -06:00
|
|
|
ctx.MaybeInitialize()
|
|
|
|
|
2018-09-14 18:24:48 -06:00
|
|
|
return ctx, nil
|
|
|
|
}
|
|
|
|
|
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"))
|
|
|
|
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()
|
|
|
|
for i := 0; i <= 9999; i += 1 {
|
|
|
|
fmt.Fprintf(f, "%04d\n", i)
|
|
|
|
}
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
|
|
|
func (ctx Instance) MothballPath(parts ...string) string {
|
|
|
|
tail := path.Join(parts...)
|
|
|
|
return path.Join(ctx.MothballDir, tail)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ctx *Instance) StatePath(parts ...string) string {
|
|
|
|
tail := path.Join(parts...)
|
|
|
|
return path.Join(ctx.StateDir, tail)
|
|
|
|
}
|
|
|
|
|
2018-09-17 18:02:44 -06:00
|
|
|
func (ctx *Instance) PointsLog() []*Award {
|
|
|
|
var ret []*Award
|
2018-09-14 18:24:48 -06:00
|
|
|
|
|
|
|
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()
|
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
|
|
|
|
}
|
2018-09-17 18:02:44 -06:00
|
|
|
ret = append(ret, cur)
|
2018-09-14 18:24:48 -06:00
|
|
|
}
|
2018-09-17 17:00:08 -06:00
|
|
|
|
2018-09-14 18:24:48 -06:00
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
2018-09-17 18:06:29 -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.
|
2018-09-17 18:02:44 -06:00
|
|
|
func (ctx *Instance) AwardPoints(teamid, category string, points int) error {
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
for _, e := range ctx.PointsLog() {
|
|
|
|
if a.Same(e) {
|
|
|
|
return fmt.Errorf("Points already awarded to this team in this category")
|
|
|
|
}
|
|
|
|
}
|
2018-09-17 21:32:24 -06:00
|
|
|
|
2018-09-17 18:06:29 -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
|
|
|
|
2018-09-17 18:06:29 -06:00
|
|
|
if err := os.Rename(tmpfn, newfn); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-09-19 17:56:26 -06:00
|
|
|
ctx.update <- true
|
2018-09-17 18:06:29 -06:00
|
|
|
log.Printf("Award %s %s %d", teamid, category, points)
|
|
|
|
return nil
|
2018-09-17 18:02:44 -06:00
|
|
|
}
|
|
|
|
|
2018-09-17 17:40:05 -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) TeamName(teamId string) (string, error) {
|
|
|
|
teamNameBytes, err := ioutil.ReadFile(ctx.StatePath("teams", teamId))
|
|
|
|
teamName := strings.TrimSpace(string(teamNameBytes))
|
|
|
|
return teamName, err
|
|
|
|
}
|