+13
-12
@@ -7,9 +7,9 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Links []Link `yaml:"links"`
|
||||
Monitor []Item `yaml:"monitor"`
|
||||
Exec []Item `yaml:"exec"`
|
||||
Links []Link `yaml:"links"`
|
||||
Monitor []Monitor `yaml:"monitor"`
|
||||
Exec []Item `yaml:"exec"`
|
||||
}
|
||||
|
||||
func loadConfig(path string) (*Config, error) {
|
||||
@@ -31,21 +31,22 @@ func loadConfig(path string) (*Config, error) {
|
||||
}
|
||||
seen[l.Name] = true
|
||||
}
|
||||
for _, list := range [][]Item{cfg.Monitor, cfg.Exec} {
|
||||
for _, it := range list {
|
||||
if seen[it.Name] {
|
||||
return nil, fmt.Errorf("duplicate name %q", it.Name)
|
||||
}
|
||||
seen[it.Name] = true
|
||||
for _, m := range cfg.Monitor {
|
||||
if seen[m.Name] {
|
||||
return nil, fmt.Errorf("duplicate name %q", m.Name)
|
||||
}
|
||||
seen[m.Name] = true
|
||||
}
|
||||
for _, it := range cfg.Exec {
|
||||
if seen[it.Name] {
|
||||
return nil, fmt.Errorf("duplicate name %q", it.Name)
|
||||
}
|
||||
seen[it.Name] = true
|
||||
}
|
||||
for _, it := range cfg.Monitor {
|
||||
if it.Refresh <= 0 {
|
||||
return nil, fmt.Errorf("monitor %q: refresh must be > 0", it.Name)
|
||||
}
|
||||
if it.Data != "text" && it.Data != "percentage" {
|
||||
return nil, fmt.Errorf("monitor %q: data must be text|percentage", it.Name)
|
||||
}
|
||||
}
|
||||
for _, it := range cfg.Exec {
|
||||
if len(it.ArgTypes) != it.Args {
|
||||
|
||||
+40
-11
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -24,11 +25,16 @@ type Item struct {
|
||||
Name string `yaml:"name"`
|
||||
Script string `yaml:"script"`
|
||||
Refresh int `yaml:"refresh"` // monitor: seconds
|
||||
Data string `yaml:"data"` // monitor: text | percentage
|
||||
Args int `yaml:"args"` // exec: arg count
|
||||
ArgTypes []string `yaml:"arg_types"` // exec: text | number per arg
|
||||
}
|
||||
|
||||
type Monitor struct {
|
||||
Name string `yaml:"name"`
|
||||
Script string `yaml:"script"`
|
||||
Refresh int `yaml:"refresh"` // seconds
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Name string `yaml:"name"`
|
||||
URL string `yaml:"url"`
|
||||
@@ -39,7 +45,7 @@ type Link struct {
|
||||
type server struct {
|
||||
cfg *Config
|
||||
exec map[string]*Item // by item name, read-only after start
|
||||
monVals sync.Map // monitor item name -> string (last output)
|
||||
monVals sync.Map // monitor item name -> *monData (last parsed output)
|
||||
docsDir string
|
||||
srvDir string // dir with *.service units (~/srv on the box)
|
||||
probeErr sync.Map // probe key -> last logged error; poll errors log once per change, not every 3s tick
|
||||
@@ -48,10 +54,34 @@ type server struct {
|
||||
|
||||
const execTimeout = 10 * time.Second
|
||||
|
||||
func runScript(s *Item, args ...string) (string, error) {
|
||||
// monData — распарсенный вывод скрипта-монитора: первая строка — процент,
|
||||
// остальные — строки key=value для подписей в карточке.
|
||||
type monData struct {
|
||||
percentage int32 // -1 = скрипт не дал процент
|
||||
stats []*homelabv1.Stat
|
||||
}
|
||||
|
||||
func parseMonitor(out string) *monData {
|
||||
d := &monData{percentage: -1}
|
||||
lines := strings.Split(strings.TrimSpace(out), "\n")
|
||||
if lines[0] == "" {
|
||||
return d
|
||||
}
|
||||
if n, err := strconv.Atoi(strings.TrimSpace(lines[0])); err == nil {
|
||||
d.percentage = int32(n)
|
||||
}
|
||||
for _, l := range lines[1:] {
|
||||
if k, v, ok := strings.Cut(strings.TrimSpace(l), "="); ok {
|
||||
d.stats = append(d.stats, &homelabv1.Stat{Name: strings.TrimSpace(k), Value: strings.TrimSpace(v)})
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func runScript(script string, args ...string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), execTimeout)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, s.Script, args...).CombinedOutput()
|
||||
out, err := exec.CommandContext(ctx, script, args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%v: %s", err, out)
|
||||
}
|
||||
@@ -62,17 +92,16 @@ func (s *server) startMonitors() {
|
||||
for i := range s.cfg.Monitor {
|
||||
sc := &s.cfg.Monitor[i]
|
||||
log.Printf("monitor started: %s (%s) every %ds", sc.Name, sc.Script, sc.Refresh)
|
||||
go func(sc *Item) {
|
||||
go func(sc *Monitor) {
|
||||
for {
|
||||
v, err := runScript(sc)
|
||||
out, err := runScript(sc.Script)
|
||||
if err != nil {
|
||||
log.Printf("monitor %s: %v", sc.Name, err)
|
||||
v = "error: " + err.Error()
|
||||
} else {
|
||||
v = strings.TrimSpace(v)
|
||||
log.Printf("monitor %s -> %s", sc.Name, v)
|
||||
out = "-\nerror=" + err.Error()
|
||||
}
|
||||
s.monVals.Store(sc.Name, v)
|
||||
d := parseMonitor(out)
|
||||
log.Printf("monitor %s -> %d%% (%d stats)", sc.Name, d.percentage, len(d.stats))
|
||||
s.monVals.Store(sc.Name, d)
|
||||
time.Sleep(time.Duration(sc.Refresh) * time.Second)
|
||||
}
|
||||
}(sc)
|
||||
|
||||
+7
-4
@@ -18,9 +18,12 @@ func (s *server) ListContent(ctx context.Context, _ *connect.Request[homelabv1.L
|
||||
}
|
||||
for i := range s.cfg.Monitor {
|
||||
it := &s.cfg.Monitor[i]
|
||||
val, _ := s.monVals.Load(it.Name)
|
||||
v, _ := val.(string)
|
||||
resp.Monitors = append(resp.Monitors, &homelabv1.Item{Name: it.Name, Data: it.Data, Value: v})
|
||||
m := &homelabv1.Monitor{Name: it.Name}
|
||||
if d, ok := s.monVals.Load(it.Name); ok {
|
||||
md := d.(*monData)
|
||||
m.Percentage, m.Stats = md.percentage, md.stats
|
||||
}
|
||||
resp.Monitors = append(resp.Monitors, m)
|
||||
}
|
||||
for i := range s.cfg.Exec {
|
||||
it := &s.cfg.Exec[i]
|
||||
@@ -38,7 +41,7 @@ func (s *server) RunScript(_ context.Context, req *connect.Request[homelabv1.Run
|
||||
if len(req.Msg.GetArgs()) != it.Args {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("%q takes %d args, got %d", it.Name, it.Args, len(req.Msg.GetArgs())))
|
||||
}
|
||||
out, err := runScript(it, req.Msg.GetArgs()...)
|
||||
out, err := runScript(it.Script, req.Msg.GetArgs()...)
|
||||
if err != nil {
|
||||
log.Printf("script %s args=%v: ERROR %v", it.Name, req.Msg.GetArgs(), err)
|
||||
return nil, connect.NewError(connect.CodeInternal, err)
|
||||
|
||||
Reference in New Issue
Block a user