package cli import ( "fmt" "os" "github.com/spf13/afero" "golang.org/x/xerrors" "github.com/coder/coder/v2/cli/clistat" "github.com/coder/coder/v2/cli/cliui" "github.com/coder/serpent" ) func initStatterMW(tgt **clistat.Statter, fs afero.Fs) serpent.MiddlewareFunc { return func(next serpent.HandlerFunc) serpent.HandlerFunc { return func(i *serpent.Invocation) error { var err error stat, err := clistat.New(clistat.WithFS(fs)) if err != nil { return xerrors.Errorf("initialize workspace stats collector: %w", err) } *tgt = stat return next(i) } } } func (r *RootCmd) stat() *serpent.Command { var ( st *clistat.Statter fs = afero.NewReadOnlyFs(afero.NewOsFs()) formatter = cliui.NewOutputFormatter( cliui.TableFormat([]statsRow{}, []string{ "host_cpu", "host_memory", "home_disk", "container_cpu", "container_memory", }), cliui.JSONFormat(), ) ) cmd := &serpent.Command{ Use: "stat", Short: "Show resource usage for the current workspace.", Middleware: initStatterMW(&st, fs), Children: []*serpent.Command{ r.statCPU(fs), r.statMem(fs), r.statDisk(fs), }, Handler: func(inv *serpent.Invocation) error { var sr statsRow // Get CPU measurements first. hostErr := make(chan error, 1) containerErr := make(chan error, 1) go func() { defer close(hostErr) cs, err := st.HostCPU() if err != nil { hostErr <- err return } sr.HostCPU = cs }() go func() { defer close(containerErr) if ok, _ := clistat.IsContainerized(fs); !ok { // don't error if we're not in a container return } cs, err := st.ContainerCPU() if err != nil { containerErr <- err return } sr.ContainerCPU = cs }() if err := <-hostErr; err != nil { return err } if err := <-containerErr; err != nil { return err } // Host-level stats ms, err := st.HostMemory(clistat.PrefixGibi) if err != nil { return err } sr.HostMemory = ms home, err := os.UserHomeDir() if err != nil { return err } ds, err := st.Disk(clistat.PrefixGibi, home) if err != nil { return err } sr.Disk = ds // Container-only stats. if ok, err := clistat.IsContainerized(fs); err == nil && ok { cs, err := st.ContainerCPU() if err != nil { return err } sr.ContainerCPU = cs ms, err := st.ContainerMemory(clistat.PrefixGibi) if err != nil { return err } sr.ContainerMemory = ms } out, err := formatter.Format(inv.Context(), []statsRow{sr}) if err != nil { return err } _, err = fmt.Fprintln(inv.Stdout, out) return err }, } formatter.AttachOptions(&cmd.Options) return cmd } func (*RootCmd) statCPU(fs afero.Fs) *serpent.Command { var ( hostArg bool st *clistat.Statter formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat()) ) cmd := &serpent.Command{ Use: "cpu", Short: "Show CPU usage, in cores.", Middleware: initStatterMW(&st, fs), Options: serpent.OptionSet{ { Flag: "host", Value: serpent.BoolOf(&hostArg), Description: "Force host CPU measurement.", }, }, Handler: func(inv *serpent.Invocation) error { var cs *clistat.Result var err error if ok, _ := clistat.IsContainerized(fs); ok && !hostArg { cs, err = st.ContainerCPU() } else { cs, err = st.HostCPU() } if err != nil { return err } out, err := formatter.Format(inv.Context(), cs) if err != nil { return err } _, err = fmt.Fprintln(inv.Stdout, out) return err }, } formatter.AttachOptions(&cmd.Options) return cmd } func (*RootCmd) statMem(fs afero.Fs) *serpent.Command { var ( hostArg bool prefixArg string st *clistat.Statter formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat()) ) cmd := &serpent.Command{ Use: "mem", Short: "Show memory usage, in gigabytes.", Middleware: initStatterMW(&st, fs), Options: serpent.OptionSet{ { Flag: "host", Value: serpent.BoolOf(&hostArg), Description: "Force host memory measurement.", }, { Description: "SI Prefix for memory measurement.", Default: clistat.PrefixHumanGibi, Flag: "prefix", Value: serpent.EnumOf(&prefixArg, clistat.PrefixHumanKibi, clistat.PrefixHumanMebi, clistat.PrefixHumanGibi, clistat.PrefixHumanTebi, ), }, }, Handler: func(inv *serpent.Invocation) error { pfx := clistat.ParsePrefix(prefixArg) var ms *clistat.Result var err error if ok, _ := clistat.IsContainerized(fs); ok && !hostArg { ms, err = st.ContainerMemory(pfx) } else { ms, err = st.HostMemory(pfx) } if err != nil { return err } out, err := formatter.Format(inv.Context(), ms) if err != nil { return err } _, err = fmt.Fprintln(inv.Stdout, out) return err }, } formatter.AttachOptions(&cmd.Options) return cmd } func (*RootCmd) statDisk(fs afero.Fs) *serpent.Command { var ( pathArg string prefixArg string st *clistat.Statter formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat()) ) cmd := &serpent.Command{ Use: "disk", Short: "Show disk usage, in gigabytes.", Middleware: initStatterMW(&st, fs), Options: serpent.OptionSet{ { Flag: "path", Value: serpent.StringOf(&pathArg), Description: "Path for which to check disk usage.", Default: "/", }, { Flag: "prefix", Default: clistat.PrefixHumanGibi, Description: "SI Prefix for disk measurement.", Value: serpent.EnumOf(&prefixArg, clistat.PrefixHumanKibi, clistat.PrefixHumanMebi, clistat.PrefixHumanGibi, clistat.PrefixHumanTebi, ), }, }, Handler: func(inv *serpent.Invocation) error { pfx := clistat.ParsePrefix(prefixArg) // Users may also call `coder stat disk `. if len(inv.Args) > 0 { pathArg = inv.Args[0] } ds, err := st.Disk(pfx, pathArg) if err != nil { if os.IsNotExist(err) { //nolint:gocritic // fmt.Errorf produces a more concise error. return fmt.Errorf("not found: %q", pathArg) } return err } out, err := formatter.Format(inv.Context(), ds) if err != nil { return err } _, err = fmt.Fprintln(inv.Stdout, out) return err }, } formatter.AttachOptions(&cmd.Options) return cmd } type statsRow struct { HostCPU *clistat.Result `json:"host_cpu" table:"host_cpu,default_sort"` HostMemory *clistat.Result `json:"host_memory" table:"host_memory"` Disk *clistat.Result `json:"home_disk" table:"home_disk"` ContainerCPU *clistat.Result `json:"container_cpu" table:"container_cpu"` ContainerMemory *clistat.Result `json:"container_memory" table:"container_memory"` }