diff --git a/README.md b/README.md index 9d9d9c9..6b97eb6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/mothd/common.go b/cmd/mothd/common.go index ca04486..9da809b 100644 --- a/cmd/mothd/common.go +++ b/cmd/mothd/common.go @@ -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! +} \ No newline at end of file diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go new file mode 100644 index 0000000..5aa11d3 --- /dev/null +++ b/cmd/mothd/main.go @@ -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) +} \ No newline at end of file diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go new file mode 100644 index 0000000..08ee41c --- /dev/null +++ b/cmd/mothd/mothballs.go @@ -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() + } + } +} diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 43cf3b1..ff3bb62 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -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("")) - } -} diff --git a/cmd/mothd/theme.go b/cmd/mothd/theme.go index 76eeb83..895f294 100644 --- a/cmd/mothd/theme.go +++ b/cmd/mothd/theme.go @@ -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 == "/" { diff --git a/cmd/mothdv3/zipfs.go b/cmd/mothd/zipfs.go similarity index 100% rename from cmd/mothdv3/zipfs.go rename to cmd/mothd/zipfs.go diff --git a/cmd/mothdv3/zipfs_test.go b/cmd/mothd/zipfs_test.go similarity index 100% rename from cmd/mothdv3/zipfs_test.go rename to cmd/mothd/zipfs_test.go diff --git a/cmd/mothdav/main.go b/cmd/mothdav/main.go deleted file mode 100644 index d8bb201..0000000 --- a/cmd/mothdav/main.go +++ /dev/null @@ -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) - } - -} diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go index e589c71..44d05fa 100644 --- a/cmd/transpile/main.go +++ b/cmd/transpile/main.go @@ -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) diff --git a/cmd/mothdv3/instance_test.go b/cmd/transpile/mothball.go similarity index 92% rename from cmd/mothdv3/instance_test.go rename to cmd/transpile/mothball.go index 06ab7d0..c9ecbf5 100644 --- a/cmd/mothdv3/instance_test.go +++ b/cmd/transpile/mothball.go @@ -1 +1,2 @@ package main +