125 lines
3.4 KiB
Go
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
|