mirror of https://github.com/dirtbags/moth.git
no more state/disabled, better logs for hours.txt
This commit is contained in:
parent
ec2a547637
commit
bfcf325e3b
|
@ -43,6 +43,7 @@ type State struct {
|
||||||
// Enabled tracks whether the current State system is processing updates
|
// Enabled tracks whether the current State system is processing updates
|
||||||
Enabled bool
|
Enabled bool
|
||||||
|
|
||||||
|
enabledWhy string
|
||||||
refreshNow chan bool
|
refreshNow chan bool
|
||||||
eventStream chan []string
|
eventStream chan []string
|
||||||
eventWriter *csv.Writer
|
eventWriter *csv.Writer
|
||||||
|
@ -75,11 +76,10 @@ func NewState(fs afero.Fs) *State {
|
||||||
// updateEnabled checks a few things to see if this state directory is "enabled".
|
// updateEnabled checks a few things to see if this state directory is "enabled".
|
||||||
func (s *State) updateEnabled() {
|
func (s *State) updateEnabled() {
|
||||||
nextEnabled := true
|
nextEnabled := true
|
||||||
why := "`state/enabled` present, `state/hours.txt` missing"
|
why := "state/hours.txt has no timestamps before now"
|
||||||
|
|
||||||
if untilFile, err := s.Open("hours.txt"); err == nil {
|
if untilFile, err := s.Open("hours.txt"); err == nil {
|
||||||
defer untilFile.Close()
|
defer untilFile.Close()
|
||||||
why = "`state/hours.txt` present"
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(untilFile)
|
scanner := bufio.NewScanner(untilFile)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
|
@ -99,35 +99,36 @@ func (s *State) updateEnabled() {
|
||||||
case '#':
|
case '#':
|
||||||
continue
|
continue
|
||||||
default:
|
default:
|
||||||
log.Println("Misformatted line in hours.txt file")
|
log.Println("state/hours.txt has bad line:", line)
|
||||||
}
|
}
|
||||||
|
line, _, _ = strings.Cut(line, "#") // Remove inline comments
|
||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
until, err := time.Parse(time.RFC3339, line)
|
until := time.Time{}
|
||||||
if err != nil {
|
if len(line) == 0 {
|
||||||
until, err = time.Parse(RFC3339Space, line)
|
// Let it stay as zero time, so it's always before now
|
||||||
}
|
} else if until, err = time.Parse(time.RFC3339, line); err == nil {
|
||||||
if err != nil {
|
// Great, it was RFC 3339
|
||||||
log.Println("Suspended: Unparseable until date:", line)
|
} else if until, err = time.Parse(RFC3339Space, line); err == nil {
|
||||||
|
// Great, it was RFC 3339 with a space instead of a 'T'
|
||||||
|
} else {
|
||||||
|
log.Println("state/hours.txt has bad timestamp:", line)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if until.Before(time.Now()) {
|
if until.Before(time.Now()) {
|
||||||
nextEnabled = thisEnabled
|
nextEnabled = thisEnabled
|
||||||
|
why = fmt.Sprint("state/hours.txt most recent timestamp:", line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.Stat("enabled"); os.IsNotExist(err) {
|
if (nextEnabled != s.Enabled) || (why != s.enabledWhy) {
|
||||||
nextEnabled = false
|
|
||||||
why = "`state/enabled` missing"
|
|
||||||
}
|
|
||||||
|
|
||||||
if nextEnabled != s.Enabled {
|
|
||||||
s.Enabled = nextEnabled
|
s.Enabled = nextEnabled
|
||||||
log.Printf("Setting enabled=%v: %s", s.Enabled, why)
|
s.enabledWhy = why
|
||||||
|
log.Printf("Setting enabled=%v: %s", s.Enabled, s.enabledWhy)
|
||||||
if s.Enabled {
|
if s.Enabled {
|
||||||
s.LogEvent("enabled", "", "", 0, why)
|
s.LogEvent("enabled", "", "", 0, s.enabledWhy)
|
||||||
} else {
|
} else {
|
||||||
s.LogEvent("disabled", "", "", 0, why)
|
s.LogEvent("disabled", "", "", 0, s.enabledWhy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -361,21 +362,19 @@ func (s *State) maybeInitialize() {
|
||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if f, err := s.Create("enabled"); err == nil {
|
|
||||||
fmt.Fprintln(f, "enabled: remove or rename to suspend the contest.")
|
|
||||||
f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
if f, err := s.Create("hours.txt"); err == nil {
|
if f, err := s.Create("hours.txt"); err == nil {
|
||||||
fmt.Fprintln(f, "# hours.txt: when the contest is enabled")
|
fmt.Fprintln(f, "# hours.txt: when the contest is enabled")
|
||||||
fmt.Fprintln(f, "#")
|
fmt.Fprintln(f, "#")
|
||||||
fmt.Fprintln(f, "# Enable: + timestamp")
|
fmt.Fprintln(f, "# Enable: + [timestamp]")
|
||||||
fmt.Fprintln(f, "# Disable: - timestamp")
|
fmt.Fprintln(f, "# Disable: - [timestamp]")
|
||||||
fmt.Fprintln(f, "#")
|
fmt.Fprintln(f, "#")
|
||||||
fmt.Fprintln(f, "# You can have multiple start/stop times.")
|
fmt.Fprintln(f, "# This file, and all files in this directory, are re-read periodically.")
|
||||||
fmt.Fprintln(f, "# Whatever time is the most recent, wins.")
|
fmt.Fprintln(f, "# Default is enabled.")
|
||||||
fmt.Fprintln(f, "# Times in the future are ignored.")
|
fmt.Fprintln(f, "# Rules with only '-' or '+' are also allowed.")
|
||||||
|
fmt.Fprintln(f, "# Rules apply from the top down.")
|
||||||
|
fmt.Fprintln(f, "# If you put something in out of order, it's going to be bonkers.")
|
||||||
fmt.Fprintln(f)
|
fmt.Fprintln(f)
|
||||||
|
fmt.Fprintln(f, "- 1970-01-01T00:00:00Z")
|
||||||
fmt.Fprintln(f, "+", now)
|
fmt.Fprintln(f, "+", now)
|
||||||
fmt.Fprintln(f, "- 2519-10-31T00:00:00Z")
|
fmt.Fprintln(f, "- 2519-10-31T00:00:00Z")
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
|
@ -41,7 +41,6 @@ func TestState(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
mustExist("initialized")
|
mustExist("initialized")
|
||||||
mustExist("enabled")
|
|
||||||
mustExist("hours.txt")
|
mustExist("hours.txt")
|
||||||
|
|
||||||
teamIDsBuf, err := afero.ReadFile(s.Fs, "teamids.txt")
|
teamIDsBuf, err := afero.ReadFile(s.Fs, "teamids.txt")
|
||||||
|
@ -175,6 +174,9 @@ func TestStateEvents(t *testing.T) {
|
||||||
if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "init:::0" {
|
if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "init:::0" {
|
||||||
t.Error("Wrong message from event stream:", msg)
|
t.Error("Wrong message from event stream:", msg)
|
||||||
}
|
}
|
||||||
|
if msg := <-s.eventStream; !strings.HasPrefix(msg[5], "state/hours.txt") {
|
||||||
|
t.Error("Wrong message from event stream:", msg[5])
|
||||||
|
}
|
||||||
if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo:::0" {
|
if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo:::0" {
|
||||||
t.Error("Wrong message from event stream:", msg)
|
t.Error("Wrong message from event stream:", msg)
|
||||||
}
|
}
|
||||||
|
@ -196,19 +198,37 @@ func TestStateDisabled(t *testing.T) {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
defer hoursFile.Close()
|
defer hoursFile.Close()
|
||||||
|
s.refresh()
|
||||||
|
if !s.Enabled {
|
||||||
|
t.Error("Empty hours.txt not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Fprintln(hoursFile, "- 1970-01-01T01:01:01Z")
|
fmt.Fprintln(hoursFile, "- 1970-01-01T01:01:01Z")
|
||||||
hoursFile.Sync()
|
hoursFile.Sync()
|
||||||
s.refresh()
|
s.refresh()
|
||||||
if s.Enabled {
|
if s.Enabled {
|
||||||
t.Error("Disabling 1970-01-01")
|
t.Error("1970-01-01")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintln(hoursFile, "+ 1970-01-01 01:01:01+05:00")
|
fmt.Fprintln(hoursFile, "+ 1970-01-02 01:01:01+05:00")
|
||||||
hoursFile.Sync()
|
hoursFile.Sync()
|
||||||
s.refresh()
|
s.refresh()
|
||||||
if !s.Enabled {
|
if !s.Enabled {
|
||||||
t.Error("Enabling 1970-01-02")
|
t.Error("1970-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(hoursFile, "-")
|
||||||
|
hoursFile.Sync()
|
||||||
|
s.refresh()
|
||||||
|
if s.Enabled {
|
||||||
|
t.Error("bare -")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(hoursFile, "+")
|
||||||
|
hoursFile.Sync()
|
||||||
|
s.refresh()
|
||||||
|
if !s.Enabled {
|
||||||
|
t.Error("bare +")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintln(hoursFile, "")
|
fmt.Fprintln(hoursFile, "")
|
||||||
|
@ -216,7 +236,7 @@ func TestStateDisabled(t *testing.T) {
|
||||||
hoursFile.Sync()
|
hoursFile.Sync()
|
||||||
s.refresh()
|
s.refresh()
|
||||||
if !s.Enabled {
|
if !s.Enabled {
|
||||||
t.Error("Comments")
|
t.Error("Comment")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintln(hoursFile, "intentional parse error")
|
fmt.Fprintln(hoursFile, "intentional parse error")
|
||||||
|
@ -230,7 +250,7 @@ func TestStateDisabled(t *testing.T) {
|
||||||
hoursFile.Sync()
|
hoursFile.Sync()
|
||||||
s.refresh()
|
s.refresh()
|
||||||
if s.Enabled {
|
if s.Enabled {
|
||||||
t.Error("Disabling 1980-01-01")
|
t.Error("1980-01-01")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Remove("hours.txt"); err != nil {
|
if err := s.Remove("hours.txt"); err != nil {
|
||||||
|
@ -241,14 +261,6 @@ func TestStateDisabled(t *testing.T) {
|
||||||
t.Error("Removing `hours.txt` disabled event")
|
t.Error("Removing `hours.txt` disabled event")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Remove("enabled"); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
s.refresh()
|
|
||||||
if s.Enabled {
|
|
||||||
t.Error("Removing `enabled` didn't disable")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Remove("initialized")
|
s.Remove("initialized")
|
||||||
s.refresh()
|
s.refresh()
|
||||||
if !s.Enabled {
|
if !s.Enabled {
|
||||||
|
@ -303,11 +315,11 @@ func TestStateMaintainer(t *testing.T) {
|
||||||
eventLog, err := afero.ReadFile(s.Fs, "events.csv")
|
eventLog, err := afero.ReadFile(s.Fs, "events.csv")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
} else if events := strings.Split(string(eventLog), "\n"); len(events) != 3 {
|
} else if events := strings.Split(string(eventLog), "\n"); len(events) != 4 {
|
||||||
t.Log("Events:", events)
|
t.Log("Events:", events)
|
||||||
t.Error("Wrong event log length:", len(events))
|
t.Error("Wrong event log length:", len(events))
|
||||||
} else if events[2] != "" {
|
} else if events[3] != "" {
|
||||||
t.Error("Event log didn't end with newline")
|
t.Error("Event log didn't end with newline", events)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
Frequently Asked Questions
|
||||||
|
=================
|
||||||
|
|
||||||
|
I should probably move this somewhere else,
|
||||||
|
since most of it is about
|
||||||
|
|
||||||
|
Main Application Questions
|
||||||
|
=====================
|
||||||
|
|
||||||
|
## Can we add some instructions to the user interface? It's confusing.
|
||||||
|
|
||||||
|
The lack of instruction was a deliberate design decision made about 9 years ago
|
||||||
|
when we found in A/B testing that college students are a lot more motivated by
|
||||||
|
vague instruction and mystery than precise instruction. We've since found that
|
||||||
|
people who are inclined to "play" our events are similarly motivated by
|
||||||
|
weirdness and mystery: they enjoy fiddling around with things until they've
|
||||||
|
worked it out experimentally.
|
||||||
|
|
||||||
|
Oddly, the group who seems to be the most perturbed by the vagueness is
|
||||||
|
professionals. This may be because many of these folks spend long amounts of
|
||||||
|
time trying to make things accessible and precise, and this looks like a train
|
||||||
|
wreck from that perspective.
|
||||||
|
|
||||||
|
Another way to think about it: this is supposed to be a game, like Super Mario
|
||||||
|
Brothers. We were very careful about designing the puzzles so that you could
|
||||||
|
learn by playing. The whimsical design is meant to make it feel like trying
|
||||||
|
things out will not result in a catastrophic failure anywhere, and we've found
|
||||||
|
that most people figure it out very quickly without any instruction at all,
|
||||||
|
despite feeling a little confused or disoriented at first.
|
||||||
|
|
||||||
|
## Why can't I choose my team from a list when I log in?
|
||||||
|
|
||||||
|
We actually started this way, but we quickly learned that there were exploitable
|
||||||
|
attack avenues available when any participant can join any team. One individual
|
||||||
|
in 2010, having a bad day, decided to enter every answer they had, for every
|
||||||
|
team in the contest, as a way of sabotaging the event. It worked: everyone's
|
||||||
|
motivation to try and solve puzzles tanked, and people were angry that they'd
|
||||||
|
been working on content only to find that they already had the points.
|
||||||
|
|
||||||
|
## Why won't you add this helpful text to the login page?
|
||||||
|
|
||||||
|
It has been our experience that the more words we have on that page, the less
|
||||||
|
likely any of them will be read. We strive now to have no instruction at all,
|
||||||
|
and to design the interface in a way that it's obvious what you have to do.
|
||||||
|
|
||||||
|
## Why aren't we providing a link to the scoreboard?
|
||||||
|
|
||||||
|
It's because the scoreboard looks horrible on a mobile phone:
|
||||||
|
it was designed for a projector.
|
||||||
|
Once we have a scoreboard that is readable on mobile,
|
||||||
|
I'll add that link.
|
||||||
|
|
||||||
|
## Why can't we show a list of teams to log in to?
|
||||||
|
|
||||||
|
At a previous event,
|
||||||
|
we had a participant log in as other teams and solve every puzzle,
|
||||||
|
because they were upset about something.
|
||||||
|
This ruined the event for everyone,
|
||||||
|
because it took away the challenge of scoring points.
|
||||||
|
|
||||||
|
|
||||||
|
Scoreboard Questions
|
||||||
|
=================
|
||||||
|
|
||||||
|
## Why are there no links or title on the scoreboard?
|
||||||
|
|
||||||
|
The scoreboard is supposed to be projected at events, to participants. The current scoreboard isn't something we intend participants to pull up on their mobile devices or laptops.
|
||||||
|
|
||||||
|
Think of the scoreboard as sort of like the menu screens at Burger King.
|
||||||
|
|
||||||
|
|
||||||
|
## Will you change the scoreboard color scheme?
|
||||||
|
|
||||||
|
The scoreboard colors and layout were carefully chosen to be distinguishable for
|
||||||
|
all forms of color blindness, and even accessible by users with total blindness
|
||||||
|
using screen readers. This is why we decided to put the category name inside the
|
||||||
|
bar and just deal with it being a little weird.
|
||||||
|
|
||||||
|
I'm open to suggestions, but they need to work for all users.
|
|
@ -45,8 +45,8 @@ Scores
|
||||||
Pausing/resuming scoring
|
Pausing/resuming scoring
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
rm /srv/moth/state/enabled # Pause scoring
|
echo '-###' >> /srv/moth/state/hours.txt # Suspend scoring
|
||||||
touch /srv/moth/state/enabled # Resume scoring
|
sed -i '/###/d' /srv/moth/state/hours.txt # Resume scoring
|
||||||
|
|
||||||
When scoring is paused,
|
When scoring is paused,
|
||||||
participants can still submit answers,
|
participants can still submit answers,
|
||||||
|
@ -54,12 +54,13 @@ and the system will tell them whether the answer is correct.
|
||||||
As soon as you unpause,
|
As soon as you unpause,
|
||||||
all correctly-submitted answers will be scored.
|
all correctly-submitted answers will be scored.
|
||||||
|
|
||||||
|
|
||||||
Adjusting scores
|
Adjusting scores
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
rm /srv/moth/state/enabled # Suspend scoring
|
echo '-###' >> /srv/moth/state/hours.txt # Suspend scoring
|
||||||
nano /srv/moth/state/points.log # Replace nano with your preferred editor
|
nano /srv/moth/state/points.log # Replace nano with your preferred editor
|
||||||
touch /srv/moth/state/enabled # Resume scoring
|
sed -i '/###/d' /srv/moth/state/hours.txt # Resume scoring
|
||||||
|
|
||||||
We don't warn participants before we do this:
|
We don't warn participants before we do this:
|
||||||
any points scored while scoring is suspended are queued up and processed as soon as scoreing is resumed.
|
any points scored while scoring is suspended are queued up and processed as soon as scoreing is resumed.
|
||||||
|
|
|
@ -33,24 +33,13 @@ The state directory is also used to communicate actions to mothd.
|
||||||
Remove this file to reset the state. This will blow away team assignments and the points log.
|
Remove this file to reset the state. This will blow away team assignments and the points log.
|
||||||
|
|
||||||
|
|
||||||
`disabled`
|
`hours.txt`
|
||||||
----------
|
|
||||||
|
|
||||||
Create this file to pause collection of points and other maintenance.
|
|
||||||
Contestants can still submit answers,
|
|
||||||
but they won't show up on the scoreboard until you remove this file.
|
|
||||||
|
|
||||||
This file does not normally exist.
|
|
||||||
|
|
||||||
|
|
||||||
`until`
|
|
||||||
-------
|
-------
|
||||||
|
|
||||||
Put an RFC3337 date/time stamp in here to have the server pause itself at a given time.
|
A list of start and stop hours.
|
||||||
Remember that time zones exist!
|
If all the hours are in the future, the event defaults to running.
|
||||||
I recommend always using Zulu time.
|
"Stop" here just pertains to scoreboard updates and puzzle unlocking.
|
||||||
|
People can still submit answers and their awards are queued up for the next start.
|
||||||
This file does not normally exist.
|
|
||||||
|
|
||||||
|
|
||||||
`teamids.txt`
|
`teamids.txt`
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
I know you've done some work on this already but I have to get this idea down someplace.
|
||||||
|
|
||||||
|
I would like to, as much as possible, not have the same data in two different places. So here are my proposals for doing this in the filesystem backend.
|
||||||
|
|
||||||
|
We could implement all of these at the same time, although I don't know why we'd want to
|
||||||
|
|
||||||
|
### The obvious way
|
||||||
|
* [ ] Make the `teams` directory full of directories again and not files
|
||||||
|
* [ ] Move team names to `teams/$id/name`
|
||||||
|
* [ ] Define a new file called `teams/$id/map` which contains an ID that this maps to. It will only be followed once.
|
||||||
|
* [ ] When writing anything with team id, resolve any maps first
|
||||||
|
|
||||||
|
### The clever way
|
||||||
|
* [ ] Allow `teams/$id` to be a symbolic link to another team file
|
||||||
|
* [ ] When writing anything with team id, resolve any symlinks first
|
||||||
|
|
||||||
|
### The easy way
|
||||||
|
|
||||||
|
* [ ] Line 2 of a team file, optionally, contains a team id to map to
|
||||||
|
* [ ] When writing anything with a team id, resolve any maps
|
Loading…
Reference in New Issue