gokrazy/status.go

255 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package gokrazy
import (
"bytes"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"net/http"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"time"
"github.com/gokrazy/gokrazy/internal/bundled"
"github.com/gokrazy/internal/rootdev"
"rsc.io/goversion/version"
"golang.org/x/sys/unix"
)
func parseMeminfo() map[string]int64 {
meminfo, err := ioutil.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
}
var commonTmpls = mustParseCommonTmpls()
func mustParseCommonTmpls() *template.Template {
t := template.New("root")
t = template.Must(t.New("header").Parse(bundled.Asset("header.tmpl")))
t = template.Must(t.New("footer").Parse(bundled.Asset("footer.tmpl")))
return t
}
var overviewTmpl = template.Must(template.Must(commonTmpls.Clone()).New("overview").
Funcs(map[string]interface{}{
"restarting": func(t time.Time) bool {
return time.Since(t).Seconds() < 5
},
"last": func(stdout, stderr []string) string {
if len(stdout) == 0 && len(stderr) == 0 {
return ""
}
both := append(stdout, stderr...)
sort.Strings(both)
return both[len(both)-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)
},
}).
Parse(bundled.Asset("overview.tmpl")))
var statusTmpl = template.Must(template.Must(commonTmpls.Clone()).New("statusTmpl").Parse(bundled.Asset("status.tmpl")))
// mustReadFile0 returns the file contents or an empty string if the file could
// not be read. All trailing \0 bytes are stripped (as found in
// /proc/device-tree/model).
func mustReadFile0(filename string) string {
b, err := ioutil.ReadFile(filename)
if err != nil {
return ""
}
if idx := bytes.IndexByte(b, 0); idx > -1 {
b = b[:idx]
}
return string(b)
}
func model() string {
// the supported Raspberry Pis have this file
model := mustReadFile0("/proc/device-tree/model")
if model == "" {
// The PC Engines apu2c4 (and other PCs) have this file instead:
vendor := mustReadFile0("/sys/class/dmi/id/board_vendor")
name := mustReadFile0("/sys/class/dmi/id/board_name")
if vendor == "" || name == "" {
return "" // unsupported platform
}
model = strings.TrimSpace(vendor) + " " + strings.TrimSpace(name)
}
return strings.TrimSpace(model)
}
func readModuleInfo(path string) (string, error) {
v, err := version.ReadExe(path)
if err != nil {
return "", err
}
lines := strings.Split(strings.TrimSpace(v.ModuleInfo), "\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 initStatus(services []*service) {
model := model()
for _, fn := range []string{
"favicon.ico",
"bootstrap-3.3.7.min.css",
"bootstrap-table-1.11.0.min.css",
"bootstrap-table-1.11.0.min.js",
"jquery-3.1.1.min.js",
} {
http.Handle("/"+fn, bundled.HTTPHandlerFunc(fn))
}
http.HandleFunc("/status", func(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 doesnt render the
// current one unusable.
token = xsrfToken()
}
http.SetCookie(w, &http.Cookie{
Name: "gokrazy_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
}
var buf bytes.Buffer
if err := statusTmpl.Execute(&buf, struct {
Service *service
BuildTimestamp string
Hostname string
Model string
XsrfToken int32
}{
Service: svc,
BuildTimestamp: buildTimestamp,
Hostname: hostname,
Model: model,
XsrfToken: token,
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
status := "started"
if svc.Stopped() {
status = "stopped"
}
w.Header().Set("X-Gokrazy-Status", status)
w.Header().Set("X-Gokrazy-GOARCH", runtime.GOARCH)
io.Copy(w, &buf)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
var st unix.Statfs_t
if err := unix.Statfs("/perm", &st); err != nil {
log.Printf("could not stat /perm: %v", err)
}
privateAddrs, err := PrivateInterfaceAddrs()
if err != nil {
log.Printf("could not get private addrs: %v", err)
}
publicAddrs, err := PublicInterfaceAddrs()
if err != nil {
log.Printf("could not get public addrs: %v", err)
}
var buf bytes.Buffer
if err := overviewTmpl.Execute(&buf, struct {
Services []*service
PermDev string
PermUsed int64
PermAvail int64
PermTotal int64
PrivateAddrs []string
PublicAddrs []string
BuildTimestamp string
Meminfo map[string]int64
Hostname string
Model string
PARTUUID string
}{
Services: services,
PermDev: rootdev.Partition(rootdev.Perm),
PermUsed: int64(st.Bsize) * int64(st.Blocks-st.Bfree),
PermAvail: int64(st.Bsize) * int64(st.Bavail),
PermTotal: int64(st.Bsize) * int64(st.Blocks),
PrivateAddrs: privateAddrs,
PublicAddrs: publicAddrs,
BuildTimestamp: buildTimestamp,
Meminfo: parseMeminfo(),
Hostname: hostname,
Model: model,
PARTUUID: rootdev.PARTUUID(),
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.Copy(w, &buf)
})
}