no more state/disabled, better logs for hours.txt

This commit is contained in:
Neale Pickett 2022-11-29 15:33:21 -07:00
parent ec2a547637
commit bfcf325e3b
6 changed files with 165 additions and 65 deletions

View File

@ -43,6 +43,7 @@ type State struct {
// Enabled tracks whether the current State system is processing updates
Enabled bool
enabledWhy string
refreshNow chan bool
eventStream chan []string
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".
func (s *State) updateEnabled() {
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 {
defer untilFile.Close()
why = "`state/hours.txt` present"
scanner := bufio.NewScanner(untilFile)
for scanner.Scan() {
@ -99,35 +99,36 @@ func (s *State) updateEnabled() {
case '#':
continue
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)
until, err := time.Parse(time.RFC3339, line)
if err != nil {
until, err = time.Parse(RFC3339Space, line)
}
if err != nil {
log.Println("Suspended: Unparseable until date:", line)
until := time.Time{}
if len(line) == 0 {
// Let it stay as zero time, so it's always before now
} else if until, err = time.Parse(time.RFC3339, line); err == nil {
// Great, it was RFC 3339
} 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
}
if until.Before(time.Now()) {
nextEnabled = thisEnabled
why = fmt.Sprint("state/hours.txt most recent timestamp:", line)
}
}
}
if _, err := s.Stat("enabled"); os.IsNotExist(err) {
nextEnabled = false
why = "`state/enabled` missing"
}
if nextEnabled != s.Enabled {
if (nextEnabled != s.Enabled) || (why != s.enabledWhy) {
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 {
s.LogEvent("enabled", "", "", 0, why)
s.LogEvent("enabled", "", "", 0, s.enabledWhy)
} else {
s.LogEvent("disabled", "", "", 0, why)
s.LogEvent("disabled", "", "", 0, s.enabledWhy)
}
}
}
@ -361,21 +362,19 @@ func (s *State) maybeInitialize() {
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 {
fmt.Fprintln(f, "# hours.txt: when the contest is enabled")
fmt.Fprintln(f, "#")
fmt.Fprintln(f, "# Enable: + timestamp")
fmt.Fprintln(f, "# Disable: - timestamp")
fmt.Fprintln(f, "# Enable: + [timestamp]")
fmt.Fprintln(f, "# Disable: - [timestamp]")
fmt.Fprintln(f, "#")
fmt.Fprintln(f, "# You can have multiple start/stop times.")
fmt.Fprintln(f, "# Whatever time is the most recent, wins.")
fmt.Fprintln(f, "# Times in the future are ignored.")
fmt.Fprintln(f, "# This file, and all files in this directory, are re-read periodically.")
fmt.Fprintln(f, "# Default is enabled.")
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, "- 1970-01-01T00:00:00Z")
fmt.Fprintln(f, "+", now)
fmt.Fprintln(f, "- 2519-10-31T00:00:00Z")
f.Close()

View File

@ -41,7 +41,6 @@ func TestState(t *testing.T) {
}
mustExist("initialized")
mustExist("enabled")
mustExist("hours.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" {
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" {
t.Error("Wrong message from event stream:", msg)
}
@ -196,19 +198,37 @@ func TestStateDisabled(t *testing.T) {
t.Error(err)
}
defer hoursFile.Close()
s.refresh()
if !s.Enabled {
t.Error("Empty hours.txt not enabled")
}
fmt.Fprintln(hoursFile, "- 1970-01-01T01:01:01Z")
hoursFile.Sync()
s.refresh()
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()
s.refresh()
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, "")
@ -216,7 +236,7 @@ func TestStateDisabled(t *testing.T) {
hoursFile.Sync()
s.refresh()
if !s.Enabled {
t.Error("Comments")
t.Error("Comment")
}
fmt.Fprintln(hoursFile, "intentional parse error")
@ -230,7 +250,7 @@ func TestStateDisabled(t *testing.T) {
hoursFile.Sync()
s.refresh()
if s.Enabled {
t.Error("Disabling 1980-01-01")
t.Error("1980-01-01")
}
if err := s.Remove("hours.txt"); err != nil {
@ -241,14 +261,6 @@ func TestStateDisabled(t *testing.T) {
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.refresh()
if !s.Enabled {
@ -303,11 +315,11 @@ func TestStateMaintainer(t *testing.T) {
eventLog, err := afero.ReadFile(s.Fs, "events.csv")
if err != nil {
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.Error("Wrong event log length:", len(events))
} else if events[2] != "" {
t.Error("Event log didn't end with newline")
} else if events[3] != "" {
t.Error("Event log didn't end with newline", events)
}
}

79
docs/FAQ.md Normal file
View File

@ -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.

View File

@ -45,8 +45,8 @@ Scores
Pausing/resuming scoring
-------------------
rm /srv/moth/state/enabled # Pause scoring
touch /srv/moth/state/enabled # Resume scoring
echo '-###' >> /srv/moth/state/hours.txt # Suspend scoring
sed -i '/###/d' /srv/moth/state/hours.txt # Resume scoring
When scoring is paused,
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,
all correctly-submitted answers will be scored.
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
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:
any points scored while scoring is suspended are queued up and processed as soon as scoreing is resumed.

View File

@ -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.
`disabled`
----------
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`
`hours.txt`
-------
Put an RFC3337 date/time stamp in here to have the server pause itself at a given time.
Remember that time zones exist!
I recommend always using Zulu time.
This file does not normally exist.
A list of start and stop hours.
If all the hours are in the future, the event defaults to running.
"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.
`teamids.txt`

20
pids.txt Normal file
View File

@ -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