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