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}