Mostly refactored

This commit is contained in:
Neale Pickett 2019-09-02 19:47:24 -06:00
parent 54ea337447
commit 309432d05c
11 changed files with 144 additions and 153 deletions

View File

@ -36,7 +36,7 @@ And point a browser to http://localhost:8080/ (or whatever host is running the s
The development server includes a number of Python libraries that we have found useful in writing puzzles.
When you're ready to create your own puzzles,
read [the devel server documentation](docs/devel-server.md).
read [the devel server documentation](doc/devel-server.md).
Click the `[mb]` link by a puzzle category to compile and download a mothball that the production server can read.
@ -49,7 +49,7 @@ Running a Production Server
You can be more fine-grained about directories, if you like.
Inside the container, you need the following paths:
* `/state` (rw) Where state is stored. Read [the overview](docs/overview.md) to learn what's what in here.
* `/state` (rw) Where state is stored. Read [the overview](doc/overview.md) to learn what's what in here.
* `/mothballs` (ro) Mothballs (puzzle bundles) as provided by the development server.
* `/resources` (ro) Overrides for built-in HTML/CSS resources.
@ -73,7 +73,7 @@ Point a web browser at http://localhost:8080/
and start hacking on things in your `puzzles` directory.
More on how the devel sever works in
[the devel server documentation](docs/devel-server.md)
[the devel server documentation](doc/devel-server.md)
Running A Production Server

View File

@ -3,17 +3,26 @@ package main
import (
"path/filepath"
"strings"
"time"
)
func MothPath(base string, parts ...string) string {
type Component struct {
baseDir string
}
func (c *Component) path(parts ...string) string {
path := filepath.Clean(filepath.Join(parts...))
parts = filepath.SplitList(path)
for i, part := range parts {
part = strings.TrimLeft(part, "./\\:")
parts[i] = part
}
parts = append([]string{base}, parts...)
parts = append([]string{c.baseDir}, parts...)
path = filepath.Join(parts...)
path = filepath.Clean(path)
return path
}
func (c *Component) Run(updateInterval time.Duration) {
// Stub!
}

24
cmd/mothd/main.go Normal file
View File

@ -0,0 +1,24 @@
package main
import (
"time"
"log"
)
func main() {
log.Print("Started")
theme := NewTheme("../../theme")
state := NewState("../../state")
puzzles := NewMothballs("../../mothballs")
interval := 2 * time.Second
go theme.Run(interval)
go state.Run(interval)
go puzzles.Run(interval)
time.Sleep(1 * time.Second)
log.Print(state.Export(""))
time.Sleep(19 * time.Second)
}

61
cmd/mothd/mothballs.go Normal file
View File

@ -0,0 +1,61 @@
package main
import (
"time"
"io/ioutil"
"strings"
"log"
)
type Mothballs struct {
Component
categories map[string]*Zipfs
}
func NewMothballs(baseDir string) *Mothballs {
return &Mothballs{
Component: Component{
baseDir: baseDir,
},
categories: make(map[string]*Zipfs),
}
}
func (m *Mothballs) update() {
// Any new categories?
files, err := ioutil.ReadDir(m.path())
if err != nil {
log.Print("Error listing mothballs: ", err)
return
}
for _, f := range files {
filename := f.Name()
filepath := m.path(filename)
if !strings.HasSuffix(filename, ".mb") {
continue
}
categoryName := strings.TrimSuffix(filename, ".mb")
if _, ok := m.categories[categoryName]; !ok {
zfs, err := OpenZipfs(filepath)
if err != nil {
log.Print("Error opening ", filepath, ": ", err)
continue
}
log.Print("New mothball: ", filename)
m.categories[categoryName] = zfs
}
}
}
func (m *Mothballs) Run(updateInterval time.Duration) {
ticker := time.NewTicker(updateInterval)
m.update()
for {
select {
case when := <-ticker.C:
log.Print("Tick: ", when)
m.update()
}
}
}

View File

@ -1,16 +1,15 @@
package main
import (
"fmt"
"strconv"
"log"
"path/filepath"
"strings"
"os"
"io/ioutil"
"bufio"
"time"
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift
@ -34,29 +33,19 @@ type StateExport struct {
// We use the filesystem for synchronization between threads.
// The only thing State methods need to know is the path to the state directory.
type State struct {
StateDir string
update chan bool
Component
update chan bool
}
func NewState(stateDir string) (*State) {
func NewState(baseDir string) *State {
return &State{
StateDir: stateDir,
update: make(chan bool, 10),
Component: Component{
baseDir: baseDir,
},
update: make(chan bool, 10),
}
}
// Returns a cleaned up join of path parts relative to
func (s *State) path(parts ...string) string {
rel := filepath.Clean(filepath.Join(parts...))
parts = filepath.SplitList(rel)
for i, part := range parts {
part = strings.TrimLeft(part, "./\\:")
parts[i] = part
}
rel = filepath.Join(parts...)
return filepath.Join(s.StateDir, rel)
}
// Check a few things to see if this state directory is "enabled".
func (s *State) Enabled() bool {
if _, err := os.Stat(s.path("enabled")); os.IsNotExist(err) {
@ -92,19 +81,19 @@ func (s *State) TeamName(teamId string) (string, error) {
} else if err != nil {
return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamId, err)
}
return teamName, nil
}
// Write out team name. This can only be done once.
func (s *State) SetTeamName(teamId string, teamName string) error {
teamFile := s.path("teams", teamId)
err := ioutil.WriteFile(teamFile, []byte(teamName), os.ModeExclusive | 0644)
err := ioutil.WriteFile(teamFile, []byte(teamName), os.ModeExclusive|0644)
return err
}
// Retrieve the current points log
func (s *State) PointsLog() ([]*Award) {
func (s *State) PointsLog() []*Award {
pointsFile := s.path("points.log")
f, err := os.Open(pointsFile)
if err != nil {
@ -112,7 +101,7 @@ func (s *State) PointsLog() ([]*Award) {
return nil
}
defer f.Close()
pointsLog := make([]*Award, 0, 200)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
@ -130,17 +119,17 @@ func (s *State) PointsLog() ([]*Award) {
// Return an exportable points log,
// This anonymizes teamId with either an integer, or the string "self"
// for the requesting teamId.
func (s *State) Export(teamId string) (*StateExport) {
func (s *State) Export(teamId string) *StateExport {
teamName, _ := s.TeamName(teamId)
pointsLog := s.PointsLog()
export := StateExport{
PointsLog: make([]Award, len(pointsLog)),
Messages: make([]string, 0, 10),
Messages: make([]string, 0, 10),
TeamNames: map[string]string{"self": teamName},
}
// Read in messages
messagesFile := s.path("messages.txt")
if f, err := os.Open(messagesFile); err != nil {
@ -156,7 +145,7 @@ func (s *State) Export(teamId string) (*StateExport) {
export.Messages = append(export.Messages, message)
}
}
// Read in points
exportIds := map[string]string{teamId: "self"}
for logno, award := range pointsLog {
@ -167,7 +156,7 @@ func (s *State) Export(teamId string) (*StateExport) {
exportId := strconv.Itoa(logno)
exportAward.TeamId = exportId
exportIds[award.TeamId] = exportAward.TeamId
name, err := s.TeamName(award.TeamId)
if err != nil {
name = "Rodney" // https://en.wikipedia.org/wiki/Rogue_(video_game)#Gameplay
@ -176,7 +165,7 @@ func (s *State) Export(teamId string) (*StateExport) {
}
export.PointsLog[logno] = *exportAward
}
return &export
}
@ -269,10 +258,9 @@ func (s *State) collectPoints() {
}
}
func (s *State) maybeInitialize() {
// Are we supposed to re-initialize?
if _, err := os.Stat(s.path("initialized")); ! os.IsNotExist(err) {
if _, err := os.Stat(s.path("initialized")); !os.IsNotExist(err) {
return
}
@ -318,7 +306,7 @@ func (s *State) maybeInitialize() {
)
ioutil.WriteFile(
s.path("messages.txt"),
[]byte(fmt.Sprintf("[%s] Initialized.\n", time.Now())),
[]byte(fmt.Sprintf("[%s] Initialized.\n", time.Now().UTC().Format(time.RFC3339))),
0644,
)
ioutil.WriteFile(
@ -334,23 +322,10 @@ func (s *State) Run(updateInterval time.Duration) {
if s.Enabled() {
s.collectPoints()
}
select {
case <-s.update:
case <-time.After(updateInterval):
case <-s.update:
case <-time.After(updateInterval):
}
}
}
func main() {
s := NewState("./state")
go s.Run(2 * time.Second)
for {
select {
case <-time.After(5 * time.Second):
}
fmt.Println(s.Export(""))
}
}

View File

@ -7,23 +7,21 @@ import (
)
type Theme struct {
ThemeDir string
Component
}
func NewTheme(themeDir string) *Theme {
func NewTheme(baseDir string) *Theme {
return &Theme{
ThemeDir: themeDir,
Component: Component{
baseDir: baseDir,
},
}
}
func (t *Theme) path(parts ...string) string {
return MothPath(t.ThemeDir, parts...)
}
func (t *Theme) staticHandler(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path
if strings.Contains(path, "/.") {
http.Error(w, "Invalid URL path", http.StatusBadRequest)
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
if path == "/" {

View File

@ -1,82 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
"context"
"golang.org/x/net/webdav"
)
type StubLockSystem struct {
}
func (ls *StubLockSystem) Confirm(now time.Time, name0, name1 string, conditions ...webdav.Condition) (release func(), err error) {
return nil, webdav.ErrConfirmationFailed
}
func (ls *StubLockSystem) Create(now time.Time, details webdav.LockDetails) (token string, err error) {
return "", webdav.ErrLocked
}
func (ls *StubLockSystem) Refresh(now time.Time, token string, duration time.Duration) (webdav.LockDetails, error) {
return webdav.LockDetails{}, webdav.ErrNoSuchLock
}
func (ls *StubLockSystem) Unlock(now time.Time, token string) error {
return webdav.ErrNoSuchLock
}
type MothFS struct {
}
func (fs *MothFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
return os.ErrPermission
}
func (fs *MothFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
f, err := os.Open("hello.txt")
return f, err
}
func (fs *MothFS) RemoveAll(ctx context.Context, name string) error {
return os.ErrPermission
}
func (fs *MothFS) Rename(ctx context.Context, oldName, newName string) error {
return os.ErrPermission
}
func (fs *MothFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
info, err := os.Stat("hello.txt")
return info, err
}
func main() {
//dirFlag := flag.String("d", "./", "Directory to serve from. Default is CWD")
httpPort := flag.Int("p", 80, "Port to serve on (Plain HTTP)")
flag.Parse()
srv := &webdav.Handler{
FileSystem: new(MothFS),
LockSystem: new(StubLockSystem),
Logger: func(r *http.Request, err error) {
if err != nil {
log.Printf("WEBDAV [%s]: %s, ERROR: %s\n", r.Method, r.URL, err)
} else {
log.Printf("WEBDAV [%s]: %s \n", r.Method, r.URL)
}
},
}
http.Handle("/", srv)
if err := http.ListenAndServe(fmt.Sprintf(":%d", *httpPort), nil); err != nil {
log.Fatalf("Error with WebDAV server: %v", err)
}
}

View File

@ -18,18 +18,23 @@ func seedJoin(parts ...string) string {
func usage() {
out := flag.CommandLine.Output()
name := flag.CommandLine.Name()
fmt.Fprintf(out, "Usage: %s [OPTIONS] CATEGORY [CATEGORY ...]\n", name)
fmt.Fprintf(out, "Usage: %s [OPTION]... CATEGORY [PUZZLE [FILENAME]]\n", name)
fmt.Fprintf(out, "\n")
fmt.Fprintf(out, "Transpile CATEGORY, or provide individual category components.\n")
fmt.Fprintf(out, "If PUZZLE is provided, only transpile the given puzzle.\n")
fmt.Fprintf(out, "If FILENAME is provided, output provided file.\n")
flag.PrintDefaults()
}
func main() {
// XXX: We need a way to pass in "only run this one point value puzzle"
// XXX: Convert puzzle.py to standalone thingies
flag.Usage = usage
points := flag.Int("points", 0, "Transpile only this point value puzzle")
flag.Parse()
points := flag.Int("points", 0, "Transpile only this point value puzzle")
mothball := flag.Bool("mothball", false, "Generate a mothball")
flag.Parse()
baseSeedString := os.Getenv("MOTH_SEED")
jsenc := json.NewEncoder(os.Stdout)

View File

@ -1 +1,2 @@
package main