Files
mesh-catalog/modules/disk-load/cmd/disk-load/machine.go
T
jochen 6fdf5293eb
mesh/merge-gate pass: builds new: modules/disk-load, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Add disk-load: no tool said a machine's iowait, disk load or who does the I/O (hq issue 312)
2026-10-08 10:47:41 +02:00

125 lines
3.4 KiB
Go

package main
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
)
// CommandTimeout bounds the one command this module runs (the container runtime's list of names).
const CommandTimeout = 5 * time.Second
// StatfsTimeout bounds the look at one mount: a network filesystem whose server is gone blocks statfs
// for as long as it likes, and must cost the answer one line, never the whole call.
const StatfsTimeout = 2 * time.Second
// Runner runs one command and answers its standard output. It is injected so that the tools are tested
// against recorded answers rather than this machine's daemons.
type Runner func(ctx context.Context, name string, args ...string) (string, error)
// ExecRunner runs a command on the machine, bounded by CommandTimeout.
func ExecRunner(ctx context.Context, name string, args ...string) (string, error) {
ctx, cancel := context.WithTimeout(ctx, CommandTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, name, args...)
var stdout, stderr bytes.Buffer
cmd.Stdout, cmd.Stderr = &stdout, &stderr
if err := cmd.Run(); err != nil {
said := strings.TrimSpace(stderr.String())
if said != "" {
return "", fmt.Errorf("%s: %w: %s", name, err, said)
}
return "", fmt.Errorf("%s: %w", name, err)
}
return stdout.String(), nil
}
// FSStat is what statfs says of one mounted filesystem, in bytes and inodes.
type FSStat struct {
Size, Free, Avail uint64
Files, FilesFree uint64
}
// Machine is what the module reads: a filesystem root (the real one, or a test's tree standing in for
// /proc and /sys), a way to run a command, a way to wait out the sampling window, and statfs.
type Machine struct {
Root string
Run Runner
Wait func(time.Duration)
Statfs func(path string) (FSStat, error)
}
// Here is the machine this process runs on.
func Here() *Machine {
return &Machine{Root: "/", Run: ExecRunner, Wait: time.Sleep, Statfs: statfs}
}
func statfs(path string) (FSStat, error) {
type answer struct {
s FSStat
err error
}
done := make(chan answer, 1)
go func() {
var s syscall.Statfs_t
if err := syscall.Statfs(path, &s); err != nil {
done <- answer{err: err}
return
}
bs := uint64(s.Bsize)
done <- answer{s: FSStat{Size: s.Blocks * bs, Free: s.Bfree * bs, Avail: s.Bavail * bs, Files: s.Files, FilesFree: s.Ffree}}
}()
select {
case a := <-done:
return a.s, a.err
case <-time.After(StatfsTimeout):
return FSStat{}, fmt.Errorf("statfs did not answer within %s", StatfsTimeout)
}
}
func (m *Machine) path(p string) string { return filepath.Join(m.Root, p) }
// read is a file's content; "" when it cannot be read.
func (m *Machine) read(p string) string {
b, err := os.ReadFile(m.path(p))
if err != nil {
return ""
}
return string(b)
}
func (m *Machine) exists(p string) bool {
_, err := os.Stat(m.path(p))
return err == nil
}
// names lists the entries of a directory, sorted; none when it cannot be read.
func (m *Machine) names(dir string) []string {
entries, err := os.ReadDir(m.path(dir))
if err != nil {
return nil
}
out := make([]string, 0, len(entries))
for _, e := range entries {
out = append(out, e.Name())
}
return out
}
// round1 rounds to one decimal, for what a person reads.
func round1(f float64) float64 {
if f < 0 {
return -float64(int64(-f*10+0.5)) / 10
}
return float64(int64(f*10+0.5)) / 10
}
const mib = 1 << 20
const gib = 1 << 30