package main import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "time" ) // CommandTimeout bounds every command a tool or the guard runs: a journal that takes long to search // must cost a tool call twenty seconds, never the runtime's thirty. const CommandTimeout = 20 * time.Second // Runner runs one command and answers its standard output. It is injected so that every tool is // 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. A failure carries what the // command said on stderr, because "exit status 1" names nothing. 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 err := cmd.Run() if ctx.Err() == context.DeadlineExceeded { return stdout.String(), fmt.Errorf("%s did not answer within %s", name, CommandTimeout) } if err != nil { said := strings.TrimSpace(stderr.String()) if said == "" { said = strings.TrimSpace(stdout.String()) } if said != "" { return stdout.String(), fmt.Errorf("%s %s: %w: %s", name, strings.Join(args, " "), err, said) } return stdout.String(), fmt.Errorf("%s %s: %w", name, strings.Join(args, " "), err) } return stdout.String(), nil } // Machine is what the module reads and acts on: a filesystem root (the real one, or a test's tree of // /proc, /sys and /etc) and a way to run commands. type Machine struct { Root string Run Runner } // Here is the machine this process runs on. func Here() *Machine { return &Machine{Root: "/", Run: ExecRunner} } func (m *Machine) path(p string) string { return filepath.Join(m.Root, p) } // read is a file's content, trimmed; "" when it cannot be read. func (m *Machine) read(p string) string { b, err := os.ReadFile(m.path(p)) if err != nil { return "" } return strings.TrimSpace(string(b)) } // readInt is a file holding one integer; ok false when it is absent or not a number. func (m *Machine) readInt(p string) (int64, bool) { s := m.read(p) if s == "" { return 0, false } n, err := strconv.ParseInt(s, 10, 64) return n, err == nil } func (m *Machine) glob(pattern string) []string { found, _ := filepath.Glob(m.path(pattern)) out := make([]string, 0, len(found)) for _, f := range found { rel, err := filepath.Rel(m.Root, f) if err != nil { continue } out = append(out, "/"+filepath.ToSlash(rel)) } return out } // notInstalled says a command failed because it is not on this machine at all. func notInstalled(err error) bool { return errors.Is(err, exec.ErrNotFound) } // round to one decimal, for watts and percentages a person reads. func round1(f float64) float64 { return float64(int64(f*10+sign(f)*0.5)) / 10 } func sign(f float64) float64 { if f < 0 { return -1 } return 1 }