mirror of https://github.com/dirtbags/moth.git
Throttle submissions per team (more...)
* Preload team hashes in maintenance goroutine * Check team hash weirdness at maintenance time * Check team hash validity before checking answer * Rate limit answer submissions (adjustable with -attempt flag) * ... and more!
This commit is contained in:
parent
a36dbb48be
commit
1614b02b66
|
@ -0,0 +1 @@
|
||||||
|
* Figure out how to log JSend short text in addition to HTTP code
|
|
@ -0,0 +1,18 @@
|
||||||
|
Summary: Answer patterns
|
||||||
|
Answer: command.com
|
||||||
|
Answer: COMMAND.COM
|
||||||
|
Author: neale
|
||||||
|
Pattern: [0-9A-Za-z]{1,8}\.[A-Za-z]{1,3}
|
||||||
|
|
||||||
|
This puzzle features answer input pattern checking.
|
||||||
|
|
||||||
|
Sometimes you need to provide a hint about whether the user has entered the answer in the right format.
|
||||||
|
By providing a `Pattern` value (a regular expression),
|
||||||
|
the browser will (hopefully) provide a visual hint when an answer is incorrectly formatted.
|
||||||
|
It will also (hopefully) prevent the user from submitting,
|
||||||
|
which will (hopefully) inform the participant that they may have the right solution technique,
|
||||||
|
but there's a problem with the format of the answer.
|
||||||
|
This will (hopefully) keep people from getting overly-frustrated with difficult-to-enter answers.
|
||||||
|
|
||||||
|
This answer field will validate only FAT 8+3 filenames.
|
||||||
|
Try it!
|
101
src/handlers.go
101
src/handlers.go
|
@ -57,36 +57,10 @@ func hasLine(r io.Reader, line string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) registerHandler(w http.ResponseWriter, req *http.Request) {
|
func (ctx *Instance) registerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
teamname := req.FormValue("name")
|
teamName := req.FormValue("name")
|
||||||
teamid := req.FormValue("id")
|
teamId := req.FormValue("id")
|
||||||
|
|
||||||
// Keep foolish operators from shooting themselves in the foot
|
if !ctx.ValidTeamId(teamId) {
|
||||||
// You would have to add a pathname to your list of Team IDs to open this vulnerability,
|
|
||||||
// but I have learned not to overestimate people.
|
|
||||||
if strings.Contains(teamid, "../") {
|
|
||||||
teamid = "rodney"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (teamid == "") || (teamname == "") {
|
|
||||||
respond(
|
|
||||||
w, req, JSendFail,
|
|
||||||
"Invalid Entry",
|
|
||||||
"Either `id` or `name` was missing from this request.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
teamids, err := os.Open(ctx.StatePath("teamids.txt"))
|
|
||||||
if err != nil {
|
|
||||||
respond(
|
|
||||||
w, req, JSendFail,
|
|
||||||
"Cannot read valid team IDs",
|
|
||||||
"An error was encountered trying to read valid teams IDs: %v", err,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer teamids.Close()
|
|
||||||
if !hasLine(teamids, teamid) {
|
|
||||||
respond(
|
respond(
|
||||||
w, req, JSendFail,
|
w, req, JSendFail,
|
||||||
"Invalid Team ID",
|
"Invalid Team ID",
|
||||||
|
@ -95,38 +69,57 @@ func (ctx *Instance) registerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(ctx.StatePath("teams", teamid), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
f, err := os.OpenFile(ctx.StatePath("teams", teamId), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
||||||
if os.IsExist(err) {
|
if err != nil {
|
||||||
respond(
|
if os.IsExist(err) {
|
||||||
w, req, JSendFail,
|
respond(
|
||||||
"Already registered",
|
w, req, JSendFail,
|
||||||
"This team ID has already been registered.",
|
"Already registered",
|
||||||
)
|
"This team ID has already been registered.",
|
||||||
return
|
)
|
||||||
} else if err != nil {
|
} else {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
respond(
|
respond(
|
||||||
w, req, JSendFail,
|
w, req, JSendFail,
|
||||||
"Registration failed",
|
"Registration failed",
|
||||||
"Unable to register. Perhaps a teammate has already registered?",
|
"Unable to register. Perhaps a teammate has already registered?",
|
||||||
)
|
)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
fmt.Fprintln(f, teamname)
|
|
||||||
|
fmt.Fprintln(f, teamName)
|
||||||
respond(
|
respond(
|
||||||
w, req, JSendSuccess,
|
w, req, JSendSuccess,
|
||||||
"Team registered",
|
"Team registered",
|
||||||
"Okay, your team has been named and you may begin using your team ID!",
|
"Your team has been named and you may begin using your team ID!",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
teamid := req.FormValue("id")
|
teamId := req.FormValue("id")
|
||||||
category := req.FormValue("cat")
|
category := req.FormValue("cat")
|
||||||
pointstr := req.FormValue("points")
|
pointstr := req.FormValue("points")
|
||||||
answer := req.FormValue("answer")
|
answer := req.FormValue("answer")
|
||||||
|
|
||||||
|
if ! ctx.ValidTeamId(teamId) {
|
||||||
|
respond(
|
||||||
|
w, req, JSendFail,
|
||||||
|
"Invalid team ID",
|
||||||
|
"That team ID is not valid for this event.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.TooFast(teamId) {
|
||||||
|
respond(
|
||||||
|
w, req, JSendFail,
|
||||||
|
"Submitting too quickly",
|
||||||
|
"Your team can only submit one answer every %v", ctx.AttemptInterval,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
points, err := strconv.Atoi(pointstr)
|
points, err := strconv.Atoi(pointstr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respond(
|
respond(
|
||||||
|
@ -159,7 +152,7 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.AwardPoints(teamid, category, points); err != nil {
|
if err := ctx.AwardPoints(teamId, category, points); err != nil {
|
||||||
respond(
|
respond(
|
||||||
w, req, JSendError,
|
w, req, JSendError,
|
||||||
"Cannot award points",
|
"Cannot award points",
|
||||||
|
@ -170,14 +163,14 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
respond(
|
respond(
|
||||||
w, req, JSendSuccess,
|
w, req, JSendSuccess,
|
||||||
"Points awarded",
|
"Points awarded",
|
||||||
fmt.Sprintf("%d points for %s!", points, teamid),
|
fmt.Sprintf("%d points for %s!", points, teamId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) puzzlesHandler(w http.ResponseWriter, req *http.Request) {
|
func (ctx *Instance) puzzlesHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
teamid := req.FormValue("id")
|
teamId := req.FormValue("id")
|
||||||
if _, err := ctx.TeamName(teamid); err != nil {
|
if _, err := ctx.TeamName(teamId); err != nil {
|
||||||
http.Error(w, "Unauthorized: must provide team ID", http.StatusUnauthorized)
|
http.Error(w, "Must provide team ID", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +203,7 @@ func (ctx *Instance) contentHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
puzzleId := parts[len(parts)-2]
|
puzzleId := parts[len(parts)-2]
|
||||||
categoryName := parts[len(parts)-3]
|
categoryName := parts[len(parts)-3]
|
||||||
|
|
||||||
mb, ok := ctx.Categories[categoryName]
|
mb, ok := ctx.categories[categoryName]
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "Not Found", http.StatusNotFound)
|
http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
@ -238,7 +231,7 @@ func (ctx *Instance) staticHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
path = "/index.html"
|
path = "/index.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Open(ctx.ResourcePath(path))
|
f, err := os.Open(ctx.ThemePath(path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.NotFound(w, req)
|
http.NotFound(w, req)
|
||||||
return
|
return
|
||||||
|
|
|
@ -15,40 +15,39 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Instance struct {
|
type Instance struct {
|
||||||
Base string
|
Base string
|
||||||
MothballDir string
|
MothballDir string
|
||||||
StateDir string
|
StateDir string
|
||||||
ResourcesDir string
|
ThemeDir string
|
||||||
Categories map[string]*Mothball
|
AttemptInterval time.Duration
|
||||||
update chan bool
|
|
||||||
jPuzzleList []byte
|
categories map[string]*Mothball
|
||||||
jPointsLog []byte
|
update chan bool
|
||||||
mux *http.ServeMux
|
jPuzzleList []byte
|
||||||
|
jPointsLog []byte
|
||||||
|
nextAttempt map[string]time.Time
|
||||||
|
mux *http.ServeMux
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, error) {
|
func (ctx *Instance) Initialize() error {
|
||||||
ctx := &Instance{
|
// Roll over and die if directories aren't even set up
|
||||||
Base: strings.TrimRight(base, "/"),
|
if _, err := os.Stat(ctx.MothballDir); err != nil {
|
||||||
MothballDir: mothballDir,
|
return err
|
||||||
StateDir: stateDir,
|
}
|
||||||
ResourcesDir: resourcesDir,
|
if _, err := os.Stat(ctx.StateDir); err != nil {
|
||||||
Categories: map[string]*Mothball{},
|
return err
|
||||||
update: make(chan bool, 10),
|
|
||||||
mux: http.NewServeMux(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Roll over and die if directories aren't even set up
|
ctx.Base = strings.TrimRight(ctx.Base, "/")
|
||||||
if _, err := os.Stat(mothballDir); err != nil {
|
ctx.categories = map[string]*Mothball{}
|
||||||
return nil, err
|
ctx.update = make(chan bool, 10)
|
||||||
}
|
ctx.nextAttempt = map[string]time.Time{}
|
||||||
if _, err := os.Stat(stateDir); err != nil {
|
ctx.mux = http.NewServeMux()
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.BindHandlers()
|
ctx.BindHandlers()
|
||||||
ctx.MaybeInitialize()
|
ctx.MaybeInitialize()
|
||||||
|
|
||||||
return ctx, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift
|
// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift
|
||||||
|
@ -123,9 +122,16 @@ func (ctx *Instance) StatePath(parts ...string) string {
|
||||||
return path.Join(ctx.StateDir, tail)
|
return path.Join(ctx.StateDir, tail)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) ResourcePath(parts ...string) string {
|
func (ctx *Instance) ThemePath(parts ...string) string {
|
||||||
tail := pathCleanse(parts)
|
tail := pathCleanse(parts)
|
||||||
return path.Join(ctx.ResourcesDir, tail)
|
return path.Join(ctx.ThemeDir, tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Instance) TooFast(teamId string) bool {
|
||||||
|
now := time.Now()
|
||||||
|
next, _ := ctx.nextAttempt[teamId]
|
||||||
|
ctx.nextAttempt[teamId] = now.Add(ctx.AttemptInterval)
|
||||||
|
return now.Before(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) PointsLog() []*Award {
|
func (ctx *Instance) PointsLog() []*Award {
|
||||||
|
@ -153,20 +159,20 @@ func (ctx *Instance) PointsLog() []*Award {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// awardPoints gives points to teamid in category.
|
// AwardPoints gives points to teamId in category.
|
||||||
// It first checks to make sure these are not duplicate points.
|
// It first checks to make sure these are not duplicate points.
|
||||||
// This is not a perfect check, you can trigger a race condition here.
|
// This is not a perfect check, you can trigger a race condition here.
|
||||||
// It's just a courtesy to the user.
|
// It's just a courtesy to the user.
|
||||||
// The maintenance task makes sure we never have duplicate points in the log.
|
// The maintenance task makes sure we never have duplicate points in the log.
|
||||||
func (ctx *Instance) AwardPoints(teamid, category string, points int) error {
|
func (ctx *Instance) AwardPoints(teamId, category string, points int) error {
|
||||||
a := Award{
|
a := Award{
|
||||||
When: time.Now(),
|
When: time.Now(),
|
||||||
TeamId: teamid,
|
TeamId: teamId,
|
||||||
Category: category,
|
Category: category,
|
||||||
Points: points,
|
Points: points,
|
||||||
}
|
}
|
||||||
|
|
||||||
teamName, err := ctx.TeamName(teamid)
|
_, err := ctx.TeamName(teamId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("No registered team with this hash")
|
return fmt.Errorf("No registered team with this hash")
|
||||||
}
|
}
|
||||||
|
@ -177,7 +183,7 @@ func (ctx *Instance) AwardPoints(teamid, category string, points int) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn := fmt.Sprintf("%s-%s-%d", teamid, category, points)
|
fn := fmt.Sprintf("%s-%s-%d", teamId, category, points)
|
||||||
tmpfn := ctx.StatePath("points.tmp", fn)
|
tmpfn := ctx.StatePath("points.tmp", fn)
|
||||||
newfn := ctx.StatePath("points.new", fn)
|
newfn := ctx.StatePath("points.new", fn)
|
||||||
|
|
||||||
|
@ -190,12 +196,12 @@ func (ctx *Instance) AwardPoints(teamid, category string, points int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.update <- true
|
ctx.update <- true
|
||||||
log.Printf("Award %s %s %d", teamName, category, points)
|
log.Printf("Award %s %s %d", teamId, category, points)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) OpenCategoryFile(category string, parts ...string) (io.ReadCloser, error) {
|
func (ctx *Instance) OpenCategoryFile(category string, parts ...string) (io.ReadCloser, error) {
|
||||||
mb, ok := ctx.Categories[category]
|
mb, ok := ctx.categories[category]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("No such category: %s", category)
|
return nil, fmt.Errorf("No such category: %s", category)
|
||||||
}
|
}
|
||||||
|
@ -205,6 +211,11 @@ func (ctx *Instance) OpenCategoryFile(category string, parts ...string) (io.Read
|
||||||
return f, err
|
return f, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *Instance) ValidTeamId(teamId string) bool {
|
||||||
|
_, ok := ctx.nextAttempt[teamId]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *Instance) TeamName(teamId string) (string, error) {
|
func (ctx *Instance) TeamName(teamId string) (string, error) {
|
||||||
teamNameBytes, err := ioutil.ReadFile(ctx.StatePath("teams", teamId))
|
teamNameBytes, err := ioutil.ReadFile(ctx.StatePath("teams", teamId))
|
||||||
teamName := strings.TrimSpace(string(teamNameBytes))
|
teamName := strings.TrimSpace(string(teamNameBytes))
|
||||||
|
|
|
@ -40,7 +40,7 @@ func (ctx *Instance) generatePuzzleList() {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret := map[string][]PuzzleMap{}
|
ret := map[string][]PuzzleMap{}
|
||||||
for catName, mb := range ctx.Categories {
|
for catName, mb := range ctx.categories {
|
||||||
mf, err := mb.Open("map.txt")
|
mf, err := mb.Open("map.txt")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// File isn't in there
|
// File isn't in there
|
||||||
|
@ -125,12 +125,12 @@ func (ctx *Instance) tidy() {
|
||||||
ctx.MaybeInitialize()
|
ctx.MaybeInitialize()
|
||||||
|
|
||||||
// Refresh all current categories
|
// Refresh all current categories
|
||||||
for categoryName, mb := range ctx.Categories {
|
for categoryName, mb := range ctx.categories {
|
||||||
if err := mb.Refresh(); err != nil {
|
if err := mb.Refresh(); err != nil {
|
||||||
// Backing file vanished: remove this category
|
// Backing file vanished: remove this category
|
||||||
log.Printf("Removing category: %s: %s", categoryName, err)
|
log.Printf("Removing category: %s: %s", categoryName, err)
|
||||||
mb.Close()
|
mb.Close()
|
||||||
delete(ctx.Categories, categoryName)
|
delete(ctx.categories, categoryName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,18 +147,64 @@ func (ctx *Instance) tidy() {
|
||||||
}
|
}
|
||||||
categoryName := strings.TrimSuffix(filename, ".mb")
|
categoryName := strings.TrimSuffix(filename, ".mb")
|
||||||
|
|
||||||
if _, ok := ctx.Categories[categoryName]; !ok {
|
if _, ok := ctx.categories[categoryName]; !ok {
|
||||||
mb, err := OpenMothball(filepath)
|
mb, err := OpenMothball(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error opening %s: %s", filepath, err)
|
log.Printf("Error opening %s: %s", filepath, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("New category: %s", filename)
|
log.Printf("New category: %s", filename)
|
||||||
ctx.Categories[categoryName] = mb
|
ctx.categories[categoryName] = mb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if _, ok := ctx.nextAttempt[k]; !ok {
|
||||||
|
ctx.nextAttempt[k] = now
|
||||||
|
added += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For any removed team IDs, remove them
|
||||||
|
removed := 0
|
||||||
|
for k, _ := range ctx.nextAttempt {
|
||||||
|
if _, ok := newList[k]; !ok {
|
||||||
|
delete(ctx.nextAttempt, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added > 0) || (removed > 0) {
|
||||||
|
log.Printf("Team IDs updated: %d added, %d removed", added, removed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
|
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
|
||||||
// removing each points.new/ file as it goes.
|
// removing each points.new/ file as it goes.
|
||||||
func (ctx *Instance) collectPoints() {
|
func (ctx *Instance) collectPoints() {
|
||||||
|
@ -236,6 +282,7 @@ func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
|
||||||
for {
|
for {
|
||||||
if ctx.isEnabled() {
|
if ctx.isEnabled() {
|
||||||
ctx.tidy()
|
ctx.tidy()
|
||||||
|
ctx.readTeams()
|
||||||
ctx.collectPoints()
|
ctx.collectPoints()
|
||||||
ctx.generatePuzzleList()
|
ctx.generatePuzzleList()
|
||||||
ctx.generatePointsLog()
|
ctx.generatePointsLog()
|
||||||
|
|
24
src/mothd.go
24
src/mothd.go
|
@ -15,30 +15,42 @@ func setup() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
base := flag.String(
|
ctx := &Instance{}
|
||||||
|
|
||||||
|
flag.StringVar(
|
||||||
|
&ctx.Base,
|
||||||
"base",
|
"base",
|
||||||
"/",
|
"/",
|
||||||
"Base URL of this instance",
|
"Base URL of this instance",
|
||||||
)
|
)
|
||||||
mothballDir := flag.String(
|
flag.StringVar(
|
||||||
|
&ctx.MothballDir,
|
||||||
"mothballs",
|
"mothballs",
|
||||||
"/mothballs",
|
"/mothballs",
|
||||||
"Path to read mothballs",
|
"Path to read mothballs",
|
||||||
)
|
)
|
||||||
stateDir := flag.String(
|
flag.StringVar(
|
||||||
|
&ctx.StateDir,
|
||||||
"state",
|
"state",
|
||||||
"/state",
|
"/state",
|
||||||
"Path to write state",
|
"Path to write state",
|
||||||
)
|
)
|
||||||
themeDir := flag.String(
|
flag.StringVar(
|
||||||
|
&ctx.ThemeDir,
|
||||||
"theme",
|
"theme",
|
||||||
"/theme",
|
"/theme",
|
||||||
"Path to static theme resources (HTML, images, css, ...)",
|
"Path to static theme resources (HTML, images, css, ...)",
|
||||||
)
|
)
|
||||||
|
flag.DurationVar(
|
||||||
|
&ctx.AttemptInterval,
|
||||||
|
"attempt",
|
||||||
|
500*time.Millisecond,
|
||||||
|
"Per-team time required between answer attempts",
|
||||||
|
)
|
||||||
maintenanceInterval := flag.Duration(
|
maintenanceInterval := flag.Duration(
|
||||||
"maint",
|
"maint",
|
||||||
20*time.Second,
|
20*time.Second,
|
||||||
"Maintenance interval",
|
"Time between maintenance tasks",
|
||||||
)
|
)
|
||||||
listen := flag.String(
|
listen := flag.String(
|
||||||
"listen",
|
"listen",
|
||||||
|
@ -51,7 +63,7 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, err := NewInstance(*base, *mothballDir, *stateDir, *themeDir)
|
err := ctx.Initialize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue