*
Jes Olson j3s@c3f.net
Fri, 27 Dec 2024 00:17:31 -0500
4 files changed,
209 insertions(+),
155 deletions(-)
A
README
@@ -0,0 +1,5 @@
+ G O R E + GORE RUNS EVERYTHING + + RUN YOUR SERVICES + BEFORE THEY RUN YOU
A
internal/assets/status.tmpl
@@ -0,0 +1,93 @@
+{{ template "header.tmpl" . }} + +<div class="row"> + <div class="col-md-12"> + <table class="table"> + <tr> + <th>Name</th> + <th>Started</th> + <th>Actions</th> + </tr> + <tr> + <td><a href="#{{ .Service.Name }}">{{ .Service.Name }}</a></td> + <td>{{ .Service.Started.Format "Mon Jan _2 15:04:05 MST 2006" }}</td> + <td style="display: flex; flex-wrap: wrap; gap: 1em"> +{{ if .Service.Stopped }} + <form method="POST" action="/restart"> + <input type="hidden" name="xsrftoken" value="{{ .XsrfToken }}"> + <input type="hidden" name="path" value="{{ .Service.Name }}"> + <input type="hidden" name="supervise" value="once"> + <input type="submit" value="▶ run once"> + </form> + + <form method="POST" action="/restart"> + <input type="hidden" name="xsrftoken" value="{{ .XsrfToken }}"> + <input type="hidden" name="path" value="{{ .Service.Name }}"> + <input type="hidden" name="supervise" value="loop"> + <input type="submit" value="🔁 supervise (run in a loop)"> + </form> +{{ else }} + <form method="POST" action="/stop"> + <input type="hidden" name="xsrftoken" value="{{ .XsrfToken }}"> + <input type="hidden" name="path" value="{{ .Service.Name }}"> + <input type="submit" value="❌ stop"> + </form> +{{ end }} + </td> + </tr> + </table> + + {{ if (ne .Service.Diverted "") }} + <p> + Diverted: <code>{{ .Service.Diverted }}</code> + </p> + {{ end }} + + <h3>stdout <small><a href="/log?path={{ .Service.Name }}&stream=stdout">raw log</a></small></h3> + <pre id="stdout"></pre> + + <h3>stderr <small><a href="/log?path={{ .Service.Name }}&stream=stderr">raw log</a></small></h3> + <pre id="stderr"></pre> + + <h3>module info</h3> + <pre>{{ .Service.ModuleInfo }}</pre> + + </div> +</div> + +{{ template "footer.tmpl" . }} + +<script> + function newLogrotate(elt) { + const maxLines = 101; + var lines = 0; + return function (e) { + const line = e.data; + const txt = elt.innerText + line + "\n"; + lines += line.split("\n").length; + + var toRemove = lines - maxLines; + var i = 0; + while (toRemove-- > 0) { + i = txt.indexOf("\n", i) + 1; + lines--; + } + elt.innerText = txt.slice(i); + }; + } + + var stderr = new EventSource("/log?path={{ .Service.Name }}&stream=stderr", { + withCredentials: true, + }) + stderr.onmessage = newLogrotate(document.getElementById("stderr")); + + var stdout = new EventSource("/log?path={{ .Service.Name }}&stream=stdout", { + withCredentials: true, + }) + stdout.onmessage = newLogrotate(document.getElementById("stdout")); + + window.onbeforeunload = function() { + stderr.close(); + stdout.close(); + }; +</script>
M
main.go
→
main.go
@@ -5,27 +5,15 @@ "log"
"net/http" "os/exec" "path/filepath" + "time" "j3s.sh/gore/internal/assets" ) -func newService(path string) *service { - s := &service{ - cmd: exec.Command(path), - stopped: false, - } - - s.state = NewProcessState() - - tag := filepath.Base(s.Cmd().Path) - s.Stdout = newLogWriter(tag) - s.Stderr = newLogWriter(tag) - - return s -} - func main() { http.HandleFunc("GET /", indexHandler) + http.HandleFunc("GET /status", statusHandler) + http.HandleFunc("GET /log", logHandler) http.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(assets.Assets)))) http.HandleFunc("POST /add", addHandler)@@ -37,6 +25,9 @@ if err := SuperviseServices(services); err != nil {
log.Fatal(err) } log.Println("Starting Gore server on :6043...") + // arbitrarily select 90 seconds for service shutdown + // TODO: make this configurable? + defer killSupervisedServices(90 * time.Second) log.Fatal(http.ListenAndServe(":6043", nil)) }@@ -46,11 +37,22 @@ for idx, svc := range services {
unwrapped[idx] = svc } - // Only supervise services after the SIGHUP handler is set up, otherwise a - // particularly fast dhcp client (e.g. when running in qemu) might send - // SIGHUP before the signal handler is set up, thereby killing init and - // panic the system! superviseServices(unwrapped) return nil } + +func newService(path string) *service { + s := &service{ + cmd: exec.Command(path), + stopped: false, + } + + s.state = NewProcessState() + + tag := filepath.Base(s.Cmd().Path) + s.Stdout = newLogWriter(tag) + s.Stderr = newLogWriter(tag) + + return s +}
M
status.go
→
status.go
@@ -64,38 +64,6 @@ }
var modelCache atomic.Value // of string -// Model returns a human readable description of the current device model, -// e.g. “Raspberry Pi 4 Model B Rev 1.1” or “PC Engines apu2” or “QEMU” -// or ultimately “unknown model”. -func Model() string { - if s, ok := modelCache.Load().(string); ok { - return s - } - andCache := func(s string) string { - modelCache.Store(s) - return s - } - // the supported Raspberry Pis have this file - if m := readFile0("/proc/device-tree/model"); m != "" { - return andCache(m) - } - // The PC Engines apu2c4 (and other PCs) have this file instead: - vendor := readFile0("/sys/class/dmi/id/board_vendor") - name := readFile0("/sys/class/dmi/id/board_name") - if vendor != "" || name != "" { - return andCache(vendor + " " + name) - } - // QEMU has none of that. But it does say "QEMU" here, so use this as - // another fallback: - if v := readFile0("/sys/class/dmi/id/sys_vendor"); v != "" { - return andCache(v) - } - // If we can't find anything else, at least return some non-empty string so - // fbstatus doesn't render funny with empty parens. Plus this gives people - // something to grep for to add more model detection. - return "unknown model" -} - func readModuleInfo(path string) (string, error) { bi, err := buildinfo.ReadFile(path) if err != nil {@@ -183,129 +151,115 @@ },
}). ParseFS(assets.Assets, "*.tmpl")) -func initStatus() { - model := Model() +func statusHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") - var uname unix.Utsname - if err := unix.Uname(&uname); err != nil { - log.Printf("getting uname: %v", err) + token := xsrfTokenFromCookies(r.Cookies()) + if token == 0 { + // Only generate a new XSRF token if the old one is expired, so that + // loading a different form in the background doesn’t render the + // current one unusable. + token = xsrfToken() } - kernel := parseUtsname(uname) - http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") + http.SetCookie(w, &http.Cookie{ + Name: "gore_xsrf", + Value: fmt.Sprintf("%d", token), + Expires: time.Now().Add(24 * time.Hour), + HttpOnly: true, + }) - token := xsrfTokenFromCookies(r.Cookies()) - if token == 0 { - // Only generate a new XSRF token if the old one is expired, so that - // loading a different form in the background doesn’t render the - // current one unusable. - token = xsrfToken() - } - - http.SetCookie(w, &http.Cookie{ - Name: "gore_xsrf", - Value: fmt.Sprintf("%d", token), - Expires: time.Now().Add(24 * time.Hour), - HttpOnly: true, - }) + path := r.FormValue("path") + svc := findSvc(path) + if svc == nil { + http.Error(w, "service not found", http.StatusNotFound) + return + } - path := r.FormValue("path") - svc := findSvc(path) - if svc == nil { - http.Error(w, "service not found", http.StatusNotFound) + if jsonRequested(r) { + b, err := json.Marshal(svc) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) return } - if jsonRequested(r) { - b, err := json.Marshal(svc) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write(b) - return - } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(b) + return + } - var buf bytes.Buffer - if err := templates.ExecuteTemplate(&buf, "status.tmpl", struct { - Service *service - BuildTimestamp string - Hostname string - Model string - XsrfToken int32 - Kernel string - }{ - Service: svc, - Model: model, - XsrfToken: token, - Kernel: kernel, - }); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - status := "started" - if svc.Stopped() { - status = "stopped" - } - w.Header().Set("X-Gore-Status", status) - w.Header().Set("X-Gore-GOARCH", runtime.GOARCH) - io.Copy(w, &buf) - }) + var buf bytes.Buffer + if err := templates.ExecuteTemplate(&buf, "status.tmpl", struct { + Service *service + BuildTimestamp string + Hostname string + XsrfToken int32 + }{ + Service: svc, + XsrfToken: token, + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + status := "started" + if svc.Stopped() { + status = "stopped" + } + w.Header().Set("X-Gore-Status", status) + w.Header().Set("X-Gore-GOARCH", runtime.GOARCH) + io.Copy(w, &buf) +} - http.HandleFunc("/log", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") +func logHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") - evtStream := eventStreamRequested(r) - if evtStream { - w.Header().Set("Content-type", "text/event-stream") - } + evtStream := eventStreamRequested(r) + if evtStream { + w.Header().Set("Content-type", "text/event-stream") + } - path := r.FormValue("path") - svc := findSvc(path) - if svc == nil { - http.Error(w, "service not found", http.StatusNotFound) - return - } + path := r.FormValue("path") + svc := findSvc(path) + if svc == nil { + http.Error(w, "service not found", http.StatusNotFound) + return + } - streamName := r.FormValue("stream") + streamName := r.FormValue("stream") - var stream <-chan string - var closeFunc func() + var stream <-chan string + var closeFunc func() - switch streamName { - case "stdout": - stream, closeFunc = svc.Stdout.Stream() - case "stderr": - stream, closeFunc = svc.Stderr.Stream() - default: - http.Error(w, "stream not found", http.StatusNotFound) - return - } - defer closeFunc() + switch streamName { + case "stdout": + stream, closeFunc = svc.Stdout.Stream() + case "stderr": + stream, closeFunc = svc.Stderr.Stream() + default: + http.Error(w, "stream not found", http.StatusNotFound) + return + } + defer closeFunc() - for { - select { - case line := <-stream: - // See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events for description - // of server-sent events protocol. - if evtStream { - line = fmt.Sprintf("data: %s\n", line) - } - if _, err := fmt.Fprintln(w, line); err != nil { - return - } - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - case <-r.Context().Done(): - // Client closed stream. Stop and release all resources immediately. + for { + select { + case line := <-stream: + // See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events for description + // of server-sent events protocol. + if evtStream { + line = fmt.Sprintf("data: %s\n", line) + } + if _, err := fmt.Fprintln(w, line); err != nil { return } + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + case <-r.Context().Done(): + // Client closed stream. Stop and release all resources immediately. + return } - }) + } } func indexHandler(w http.ResponseWriter, r *http.Request) {