tanks/tanksd.go

189 lines
4.0 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path"
"slices"
"time"
)
var forftanksPath = flag.String("forftanks", "./forftanks", "path to forftanks executable")
var wwwDir = flag.String("www", "www", "path to www http content (ro)")
var tanksDir = flag.String("tanks", "tanks", "path to tanks state directories (rw)")
var roundsDir = flag.String("rounds", "rounds", "path to rounds storage (rw)")
var maxrounds = flag.Uint("maxrounds", 200, "number of rounds to store")
var maxSize = flag.Uint("maxsize", 8000 , "maximum uploaded file size")
var listenAddr = flag.String("listen", ":8080", "where to listen for incoming HTTP connections")
var roundDuration = flag.Duration("round", 1 * time.Minute, "Time to wait between each round")
type TankState struct {
dir string
roundsdir string
}
var validFilenames = []string{
"author",
"name",
"color",
"program",
"sensor0",
"sensor1",
"sensor2",
"sensor3",
"sensor4",
"sensor5",
"sensor6",
"sensor7",
"sensor8",
"sensor9",
}
func (ts *TankState) ServeHTTP(w http.ResponseWriter, req *http.Request) {
id := req.PathValue("id")
name := req.PathValue("name")
if req.ContentLength < 0 {
http.Error(w, "Length required", http.StatusLengthRequired)
return
}
if uint(req.ContentLength) > *maxSize {
http.Error(w, "Too large", http.StatusRequestEntityTooLarge)
return
}
if !slices.Contains(validFilenames, name) {
http.Error(w, "Invalid filename", http.StatusNotFound)
return
}
tankDir := path.Join(ts.dir, id)
if tankDir == ts.dir {
http.Error(w, "Invalid tank ID", http.StatusBadRequest)
return
}
filename := path.Join(tankDir, name)
f, err := os.Create(filename)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer f.Close()
if _, err := io.Copy(f, req.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "%s/%s: written\n", id, name)
}
func (ts *TankState) WriteRound(now time.Time, round []byte) error {
// Write new round
roundFn := fmt.Sprintf("%016x.json", now.Unix())
roundPath := path.Join(ts.roundsdir, roundFn)
if err := os.WriteFile(roundPath, round, 0644); err != nil {
return err
}
// Clean up and index all rounds
dents, err := os.ReadDir(ts.roundsdir)
if err != nil {
return err
}
for uint(len(dents)) > *maxrounds {
fn := path.Join(ts.roundsdir, dents[0].Name())
if err := os.Remove(fn); err != nil {
return err
}
dents = dents[1:]
}
rounds := make([]string, 0, len(dents))
for i := 0; i < len(dents); i++ {
name := dents[i].Name()
switch name {
case "index.json":
continue
}
rounds = append(rounds, name)
}
roundsJs, err := json.Marshal(rounds)
if err != nil {
return err
}
idxFn := path.Join(ts.roundsdir, "index.json")
if err := os.WriteFile(idxFn, roundsJs, 0644); err != nil {
return err
}
return nil
}
func (ts *TankState) RunRound(now time.Time) error {
dents, err := os.ReadDir(ts.dir)
if err != nil {
return err
}
args := make([]string, 0, len(dents))
for _, dent := range dents {
if dent.IsDir() {
tankPath := path.Join(ts.dir, dent.Name())
args = append(args, tankPath)
}
}
if len(args) < 2 {
return fmt.Errorf("Not enough tanks for a round")
}
cmd := exec.Command(*forftanksPath, args...)
out, err := cmd.Output()
if err != nil {
return err
}
if err := ts.WriteRound(now, out); err != nil {
return err
}
return nil
}
func (ts *TankState) RunForever() {
if err := ts.RunRound(time.Now()); err != nil {
log.Println(err)
}
for now := range time.Tick(*roundDuration) {
if err := ts.RunRound(now); err != nil {
log.Println(err)
}
}
}
func main() {
flag.Parse()
ts := &TankState{
dir: *tanksDir,
roundsdir: *roundsDir,
}
http.Handle("GET /", http.FileServer(http.Dir(*wwwDir)))
http.Handle("GET /rounds/", http.StripPrefix("/rounds/", http.FileServer(http.Dir(*roundsDir))))
http.Handle("PUT /tanks/{id}/{name}", ts)
go ts.RunForever()
log.Println("Listening on", *listenAddr)
http.ListenAndServe(*listenAddr, nil)
}