tanks

Blow up enemy tanks using code
git clone https://git.woozle.org/neale/tanks.git

Neale Pickett  ·  2024-12-04

tanksd.go

  1package main
  2
  3import (
  4	"encoding/json"
  5	"flag"
  6	"fmt"
  7	"io"
  8	"log"
  9	"net/http"
 10	"os"
 11	"os/exec"
 12	"path"
 13	"slices"
 14	"time"
 15)
 16
 17var forftanksPath = flag.String("forftanks", "./forftanks", "path to forftanks executable")
 18var wwwDir = flag.String("www", "www", "path to www http content (ro)")
 19var tanksDir = flag.String("tanks", "tanks", "path to tanks state directories (rw)")
 20var roundsDir = flag.String("rounds", "rounds", "path to rounds storage (rw)")
 21var maxrounds = flag.Uint("maxrounds", 200, "number of rounds to store")
 22var maxSize = flag.Uint("maxsize", 8000 , "maximum uploaded file size")
 23var listenAddr = flag.String("listen", ":8080", "where to listen for incoming HTTP connections")
 24var roundDuration = flag.Duration("round", 1 * time.Minute, "Time to wait between each round")
 25
 26type TankState struct {
 27	dir string
 28	roundsdir string
 29}
 30
 31var validFilenames = []string{
 32	"author",
 33	"name",
 34	"color",
 35	"program",
 36	"sensor0",
 37	"sensor1",
 38	"sensor2",
 39	"sensor3",
 40	"sensor4",
 41	"sensor5",
 42	"sensor6",
 43	"sensor7",
 44	"sensor8",
 45	"sensor9",
 46}
 47func (ts *TankState) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 48	id := req.PathValue("id")
 49	name := req.PathValue("name")
 50
 51	if req.ContentLength < 0 {
 52		http.Error(w, "Length required", http.StatusLengthRequired)
 53		return
 54	}
 55	if uint(req.ContentLength) > *maxSize {
 56		http.Error(w, "Too large", http.StatusRequestEntityTooLarge)
 57		return
 58	}
 59
 60	if !slices.Contains(validFilenames, name) {
 61		http.Error(w, "Invalid filename", http.StatusNotFound)
 62		return
 63	}
 64
 65	tankDir := path.Join(ts.dir, id)
 66	if tankDir == ts.dir {
 67		http.Error(w, "Invalid tank ID", http.StatusBadRequest)
 68		return
 69	}
 70
 71	filename := path.Join(tankDir, name)
 72	f, err := os.Create(filename)
 73	if err != nil {
 74		http.Error(w, err.Error(), http.StatusBadRequest)
 75		return
 76	}
 77	defer f.Close()
 78	if _, err := io.Copy(f, req.Body); err != nil {
 79		http.Error(w, err.Error(), http.StatusInternalServerError)
 80		return
 81	}
 82
 83	fmt.Fprintf(w, "%s/%s: written\n", id, name)
 84}
 85
 86func (ts *TankState) WriteRound(now time.Time, round []byte) error {
 87	// Write new round
 88	roundFn := fmt.Sprintf("%016x.json", now.Unix())
 89	roundPath := path.Join(ts.roundsdir, roundFn)
 90	if err := os.WriteFile(roundPath, round, 0644); err != nil {
 91		return err
 92	}
 93
 94	// Clean up and index all rounds
 95	dents, err := os.ReadDir(ts.roundsdir)
 96	if err != nil {
 97		return err
 98	}
 99	for uint(len(dents)) > *maxrounds {
100		fn := path.Join(ts.roundsdir, dents[0].Name())
101		if err := os.Remove(fn); err != nil {
102			return err
103		}
104		dents = dents[1:]
105	}
106
107	rounds := make([]string, 0, len(dents))
108	for i := 0; i < len(dents); i++ {
109		name := dents[i].Name()
110		switch name {
111		case "index.json":
112			continue
113		}
114		rounds = append(rounds, name)
115	}
116
117	roundsJs, err := json.Marshal(rounds)
118	if err != nil {
119		return err
120	}
121	idxFn := path.Join(ts.roundsdir, "index.json")
122	if err := os.WriteFile(idxFn, roundsJs, 0644); err != nil {
123		return err
124	}
125
126	return nil
127}
128
129func (ts *TankState) RunRound(now time.Time) error {
130	dents, err := os.ReadDir(ts.dir)
131	if err != nil {
132		return err
133	}
134
135	args := make([]string, 0, len(dents))
136	for _, dent := range dents {
137		if dent.IsDir() {
138			tankPath := path.Join(ts.dir, dent.Name())
139			args = append(args, tankPath)
140		}
141	}
142
143	if len(args) < 2 {
144		return fmt.Errorf("Not enough tanks for a round")
145	}
146
147	cmd := exec.Command(*forftanksPath, args...)
148	out, err := cmd.Output()
149	if err != nil {
150		return err
151	}
152
153	if err := ts.WriteRound(now, out); err != nil {
154		return err
155	}
156	
157	return nil
158}
159
160func (ts *TankState) RunForever() {
161	if err := ts.RunRound(time.Now()); err != nil {
162		log.Println(err)
163	}
164
165	for now := range time.Tick(*roundDuration) {
166		if err := ts.RunRound(now); err != nil {
167			log.Println(err)
168		}
169	}
170}
171
172func main() {
173	flag.Parse()
174
175	ts := &TankState{
176		dir: *tanksDir,
177		roundsdir: *roundsDir,
178	}
179
180	http.Handle("GET /", http.FileServer(http.Dir(*wwwDir)))
181	http.Handle("GET /rounds/", http.StripPrefix("/rounds/", http.FileServer(http.Dir(*roundsDir))))
182	http.Handle("PUT /tanks/{id}/{name}", ts)
183
184	go ts.RunForever()
185
186	log.Println("Listening on", *listenAddr)
187	http.ListenAndServe(*listenAddr, nil)
188}