small pixel drawing of a pufferfish gore

status.go

package main

import (
	"bytes"
	"context"
	"debug/buildinfo"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync/atomic"
	"time"

	"j3s.sh/gore/internal/assets"
	"j3s.sh/gore/internal/module"

	"golang.org/x/sys/unix"
)

func parseMeminfo() map[string]int64 {
	meminfo, err := os.ReadFile("/proc/meminfo")
	if err != nil {
		return nil
	}
	vals := make(map[string]int64)
	for _, line := range strings.Split(string(meminfo), "\n") {
		if !strings.HasPrefix(line, "MemTotal") &&
			!strings.HasPrefix(line, "MemAvailable") {
			continue
		}
		parts := strings.Split(line, ":")
		if len(parts) < 2 {
			continue
		}
		val, err := strconv.ParseInt(strings.TrimSpace(strings.TrimSuffix(parts[1], " kB")), 0, 64)
		if err != nil {
			continue
		}
		vals[parts[0]] = val * 1024 // KiB to B
	}
	return vals
}

// readFile0 returns the file contents or an empty string if the file could not
// be read. All bytes from any \0 byte onwards are stripped (as found in
// /proc/device-tree/model).
//
// Additionally, whitespace is trimmed.
func readFile0(filename string) string {
	b, _ := os.ReadFile(filename)
	if idx := bytes.IndexByte(b, 0); idx > -1 {
		b = b[:idx]
	}
	return string(bytes.TrimSpace(b))
}

var modelCache atomic.Value // of string

func readModuleInfo(path string) (string, error) {
	bi, err := buildinfo.ReadFile(path)
	if err != nil {
		return "", err
	}
	lines := strings.Split(strings.TrimSpace(bi.String()), "\n")
	shortened := make([]string, len(lines))
	for idx, line := range lines {
		row := strings.Split(line, "\t")
		if len(row) > 3 {
			row = row[:3]
		}
		shortened[idx] = strings.Join(row, "\t")
	}
	return strings.Join(shortened, "\n"), nil
}

func parseUtsname(u unix.Utsname) string {
	if u == (unix.Utsname{}) {
		// Empty utsname, no info to parse.
		return "unknown"
	}

	str := func(b [65]byte) string {
		// Trim all trailing NULL bytes.
		return string(bytes.TrimRight(b[:], "\x00"))
	}

	return fmt.Sprintf("%s %s (%s)",
		str(u.Sysname), str(u.Release), str(u.Machine))
}

func jsonRequested(r *http.Request) bool {
	// When this function was introduced, it incorrectly checked the
	// Content-Type header (which specifies the type of the body, if any), where
	// it should have looked at the Accept header. Hence, we now consider both,
	// at least for some time.
	return strings.Contains(strings.ToLower(r.Header.Get("Accept")), "application/json") ||
		strings.Contains(strings.ToLower(r.Header.Get("Content-type")), "application/json")
}

func eventStreamRequested(r *http.Request) bool {
	return strings.Contains(strings.ToLower(r.Header.Get("Accept")), "text/event-stream")
}

var templates = template.Must(template.New("root").
	Funcs(map[string]interface{}{
		"shortenSHA256": func(hash string) string {
			if len(hash) > 10 {
				return hash[:10]
			}
			return hash
		},
		"restarting": func(t time.Time) bool {
			return time.Since(t).Seconds() < 5
		},

		"last": func(s []string) string {
			if len(s) == 0 {
				return ""
			}
			return s[len(s)-1]
		},

		"megabytes": func(val int64) string {
			return fmt.Sprintf("%.1f MiB", float64(val)/1024/1024)
		},

		"gigabytes": func(val int64) string {
			return fmt.Sprintf("%.1f GiB", float64(val)/1024/1024/1024)
		},

		"baseName": func(path string) string {
			return filepath.Base(path)
		},

		"initRss": func() int64 {
			return rssOfPid(1)
		},

		"rssPercentage": func(meminfo map[string]int64, rss int64) string {
			used := float64(meminfo["MemTotal"] - meminfo["MemAvailable"])
			return fmt.Sprintf("%.f", float64(rss)/used*100)
		},
	}).
	ParseFS(assets.Assets, "*.tmpl"))

func statusHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Access-Control-Allow-Origin", "*")

	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
	}

	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
	}

	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)
}

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")
	}

	path := r.FormValue("path")
	svc := findSvc(path)
	if svc == nil {
		http.Error(w, "service not found", http.StatusNotFound)
		return
	}

	streamName := r.FormValue("stream")

	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()

	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) {
	services.Lock()
	defer services.Unlock()
	status := struct {
		Services []*service
		Meminfo  map[string]int64
	}{
		Services: services.S,
		Meminfo:  parseMeminfo(),
	}

	if jsonRequested(r) {
		b, err := json.Marshal(status)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write(b)
		return
	}

	var buf bytes.Buffer
	if err := templates.ExecuteTemplate(&buf, "overview.tmpl", status); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	io.Copy(w, &buf)
}

func addHandler(w http.ResponseWriter, r *http.Request) {
	formInput := r.FormValue("path")
	if formInput == "" {
		http.Error(w, "Module URL is required (e.g. git.j3s.sh/vore@latest)", http.StatusBadRequest)
		return
	}

	path, version, found := strings.Cut(formInput, "@")
	fmt.Println(path, version)
	if !found {
		version = "latest"
	}

	// First:  we resolve da module using le proxies
	resolved, err := module.Resolve(context.TODO(), path, version)
	if err != nil {
		msg := fmt.Sprintf("Failed to resolve module: %v", err)
		fmt.Fprint(w, msg)
		log.Println(msg)
		return
	}

	log.Printf(`Adding the following package to gore:
  Go package  : %s
  in Go module: %s`, path, resolved.Module)

	// Next: we install da binary, based on le module data
	buildDir := filepath.Join("builddir", resolved.Module)
	if _, err := os.Stat(buildDir); err != nil {
		log.Printf("Creating builddir for module %s", resolved.Module)
		if err := os.MkdirAll(buildDir, 0755); err != nil {
			log.Printf("Failed to create builddir for module %s: %v", resolved.Module, err)
			return
		}
	}

	get := exec.Command("go", "install", resolved.Module+"@"+resolved.Version)
	absoluteInstallPath, err := filepath.Abs(filepath.Join(buildDir + "@" + resolved.Version))
	if err != nil {
		fmt.Fprintf(w, "500 internal server error\n%s", err)
		log.Println(err)
		return
	}
	get.Env = append(os.Environ(), "CGO_ENABLED=0", "GOBIN="+absoluteInstallPath)
	get.Stdout = os.Stdout
	get.Stderr = os.Stderr
	if err := get.Run(); err != nil {
		fmt.Fprintf(w, "500 internal server error\n%s", err)
		log.Println(err)
		return
	}

	svc := newService(filepath.Join(absoluteInstallPath, filepath.Base(path)))
	superviseService(svc)
	http.Redirect(w, r, "/", http.StatusSeeOther)
}