From 024cce5849c8771f3afc22ea334ae844ec6418e3 Mon Sep 17 00:00:00 2001 From: Glib Smaga Date: Tue, 28 Jun 2022 09:14:59 -0700 Subject: [PATCH] internal/cmd: Move code here from the main package --- cmd.go | 211 ------------------------- internal/cmd/process.go | 104 ++++++++++++ internal/cmd/process_test.go | 49 ++++++ internal/cmd/root.go | 171 ++++++++++++++++++++ internal/cmd/root_test.go | 38 +++++ internal/cmd/shared.go | 216 +++++++++++++++++++++++++ internal/cmd/tree.go | 56 +++++++ main.go | 296 +---------------------------------- main_test.go | 75 --------- 9 files changed, 636 insertions(+), 580 deletions(-) create mode 100644 internal/cmd/process.go create mode 100644 internal/cmd/process_test.go create mode 100644 internal/cmd/root.go create mode 100644 internal/cmd/root_test.go create mode 100644 internal/cmd/shared.go create mode 100644 internal/cmd/tree.go diff --git a/cmd.go b/cmd.go index ba63506..175eb2a 100644 --- a/cmd.go +++ b/cmd.go @@ -3,214 +3,3 @@ // license that can be found in the LICENSE file. package main - -import ( - "encoding/binary" - "errors" - "fmt" - "io" - "io/ioutil" - "net" - "os" - "os/exec" - "strconv" - "strings" - - "github.com/google/gops/internal" - "github.com/google/gops/signal" -) - -var cmds = map[string](func(addr net.TCPAddr, params []string) error){ - "stack": stackTrace, - "gc": gc, - "memstats": memStats, - "version": version, - "pprof-heap": pprofHeap, - "pprof-cpu": pprofCPU, - "stats": stats, - "trace": trace, - "setgc": setGC, -} - -func setGC(addr net.TCPAddr, params []string) error { - if len(params) != 1 { - return errors.New("missing gc percentage") - } - perc, err := strconv.ParseInt(params[0], 10, strconv.IntSize) - if err != nil { - return err - } - buf := make([]byte, binary.MaxVarintLen64) - binary.PutVarint(buf, perc) - return cmdWithPrint(addr, signal.SetGCPercent, buf...) -} - -func stackTrace(addr net.TCPAddr, _ []string) error { - return cmdWithPrint(addr, signal.StackTrace) -} - -func gc(addr net.TCPAddr, _ []string) error { - _, err := cmd(addr, signal.GC) - return err -} - -func memStats(addr net.TCPAddr, _ []string) error { - return cmdWithPrint(addr, signal.MemStats) -} - -func version(addr net.TCPAddr, _ []string) error { - return cmdWithPrint(addr, signal.Version) -} - -func pprofHeap(addr net.TCPAddr, _ []string) error { - return pprof(addr, signal.HeapProfile, "heap") -} - -func pprofCPU(addr net.TCPAddr, _ []string) error { - fmt.Println("Profiling CPU now, will take 30 secs...") - return pprof(addr, signal.CPUProfile, "cpu") -} - -func trace(addr net.TCPAddr, _ []string) error { - fmt.Println("Tracing now, will take 5 secs...") - out, err := cmd(addr, signal.Trace) - if err != nil { - return err - } - if len(out) == 0 { - return errors.New("nothing has traced") - } - tmpfile, err := ioutil.TempFile("", "trace") - if err != nil { - return err - } - if err := ioutil.WriteFile(tmpfile.Name(), out, 0); err != nil { - return err - } - fmt.Printf("Trace dump saved to: %s\n", tmpfile.Name()) - // If go tool chain not found, stopping here and keep trace file. - if _, err := exec.LookPath("go"); err != nil { - return nil - } - defer os.Remove(tmpfile.Name()) - cmd := exec.Command("go", "tool", "trace", tmpfile.Name()) - cmd.Env = os.Environ() - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} - -func pprof(addr net.TCPAddr, p byte, prefix string) error { - tmpDumpFile, err := ioutil.TempFile("", prefix+"_profile") - if err != nil { - return err - } - { - out, err := cmd(addr, p) - if err != nil { - return err - } - if len(out) == 0 { - return errors.New("failed to read the profile") - } - if err := ioutil.WriteFile(tmpDumpFile.Name(), out, 0); err != nil { - return err - } - fmt.Printf("Profile dump saved to: %s\n", tmpDumpFile.Name()) - // If go tool chain not found, stopping here and keep dump file. - if _, err := exec.LookPath("go"); err != nil { - return nil - } - defer os.Remove(tmpDumpFile.Name()) - } - // Download running binary - tmpBinFile, err := ioutil.TempFile("", "binary") - if err != nil { - return err - } - { - out, err := cmd(addr, signal.BinaryDump) - if err != nil { - return fmt.Errorf("failed to read the binary: %v", err) - } - if len(out) == 0 { - return errors.New("failed to read the binary") - } - defer os.Remove(tmpBinFile.Name()) - if err := ioutil.WriteFile(tmpBinFile.Name(), out, 0); err != nil { - return err - } - } - fmt.Printf("Binary file saved to: %s\n", tmpBinFile.Name()) - cmd := exec.Command("go", "tool", "pprof", tmpBinFile.Name(), tmpDumpFile.Name()) - cmd.Env = os.Environ() - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} - -func stats(addr net.TCPAddr, _ []string) error { - return cmdWithPrint(addr, signal.Stats) -} - -func cmdWithPrint(addr net.TCPAddr, c byte, params ...byte) error { - out, err := cmd(addr, c, params...) - if err != nil { - return err - } - fmt.Printf("%s", out) - return nil -} - -// targetToAddr tries to parse the target string, be it remote host:port -// or local process's PID. -func targetToAddr(target string) (*net.TCPAddr, error) { - if strings.Contains(target, ":") { - // addr host:port passed - var err error - addr, err := net.ResolveTCPAddr("tcp", target) - if err != nil { - return nil, fmt.Errorf("couldn't parse dst address: %v", err) - } - return addr, nil - } - // try to find port by pid then, connect to local - pid, err := strconv.Atoi(target) - if err != nil { - return nil, fmt.Errorf("couldn't parse PID: %v", err) - } - port, err := internal.GetPort(pid) - if err != nil { - return nil, fmt.Errorf("couldn't get port for PID %v: %v", pid, err) - } - addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:"+port) - return addr, nil -} - -func cmd(addr net.TCPAddr, c byte, params ...byte) ([]byte, error) { - conn, err := cmdLazy(addr, c, params...) - if err != nil { - return nil, fmt.Errorf("couldn't get port by PID: %v", err) - } - - all, err := ioutil.ReadAll(conn) - if err != nil { - return nil, err - } - return all, nil -} - -func cmdLazy(addr net.TCPAddr, c byte, params ...byte) (io.Reader, error) { - conn, err := net.DialTCP("tcp", nil, &addr) - if err != nil { - return nil, err - } - buf := []byte{c} - buf = append(buf, params...) - if _, err := conn.Write(buf); err != nil { - return nil, err - } - return conn, nil -} diff --git a/internal/cmd/process.go b/internal/cmd/process.go new file mode 100644 index 0000000..d4488ca --- /dev/null +++ b/internal/cmd/process.go @@ -0,0 +1,104 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmd + +import ( + "fmt" + "log" + "math" + "strings" + "time" + + "github.com/shirou/gopsutil/v3/process" +) + +func processInfo(pid int, period time.Duration) { + if period < 0 { + log.Fatalf("Cannot determine CPU usage for negative duration %v", period) + } + p, err := process.NewProcess(int32(pid)) + if err != nil { + log.Fatalf("Cannot read process info: %v", err) + } + if v, err := p.Parent(); err == nil { + fmt.Printf("parent PID:\t%v\n", v.Pid) + } + if v, err := p.NumThreads(); err == nil { + fmt.Printf("threads:\t%v\n", v) + } + if v, err := p.MemoryPercent(); err == nil { + fmt.Printf("memory usage:\t%.3f%%\n", v) + } + if v, err := p.CPUPercent(); err == nil { + fmt.Printf("cpu usage:\t%.3f%%\n", v) + } + if period > 0 { + if v, err := cpuPercentWithinTime(p, period); err == nil { + fmt.Printf("cpu usage (%v):\t%.3f%%\n", period, v) + } + } + if v, err := p.Username(); err == nil { + fmt.Printf("username:\t%v\n", v) + } + if v, err := p.Cmdline(); err == nil { + fmt.Printf("cmd+args:\t%v\n", v) + } + if v, err := elapsedTime(p); err == nil { + fmt.Printf("elapsed time:\t%v\n", v) + } + if v, err := p.Connections(); err == nil { + if len(v) > 0 { + for _, conn := range v { + fmt.Printf("local/remote:\t%v:%v <-> %v:%v (%v)\n", + conn.Laddr.IP, conn.Laddr.Port, conn.Raddr.IP, conn.Raddr.Port, conn.Status) + } + } + } +} + +// cpuPercentWithinTime return how many percent of the CPU time this process uses within given time duration +func cpuPercentWithinTime(p *process.Process, t time.Duration) (float64, error) { + cput, err := p.Times() + if err != nil { + return 0, err + } + time.Sleep(t) + cput2, err := p.Times() + if err != nil { + return 0, err + } + return 100 * (cput2.Total() - cput.Total()) / t.Seconds(), nil +} + +// elapsedTime shows the elapsed time of the process indicating how long the +// process has been running for. +func elapsedTime(p *process.Process) (string, error) { + crtTime, err := p.CreateTime() + if err != nil { + return "", err + } + etime := time.Since(time.Unix(crtTime/1000, 0)) + return fmtEtimeDuration(etime), nil +} + +// fmtEtimeDuration formats etime's duration based on ps' format: +// [[DD-]hh:]mm:ss +// format specification: http://linuxcommand.org/lc3_man_pages/ps1.html +func fmtEtimeDuration(d time.Duration) string { + days := d / (24 * time.Hour) + hours := d % (24 * time.Hour) + minutes := hours % time.Hour + seconds := math.Mod(minutes.Seconds(), 60) + var b strings.Builder + if days > 0 { + fmt.Fprintf(&b, "%02d-", days) + } + if days > 0 || hours/time.Hour > 0 { + fmt.Fprintf(&b, "%02d:", hours/time.Hour) + } + fmt.Fprintf(&b, "%02d:", minutes/time.Minute) + fmt.Fprintf(&b, "%02.0f", seconds) + return b.String() +} diff --git a/internal/cmd/process_test.go b/internal/cmd/process_test.go new file mode 100644 index 0000000..4e43dc9 --- /dev/null +++ b/internal/cmd/process_test.go @@ -0,0 +1,49 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmd + +import ( + "testing" + "time" +) + +func Test_fmtEtimeDuration(t *testing.T) { + tests := []struct { + d time.Duration + want string + }{ + { + want: "00:00", + }, + + { + d: 2*time.Minute + 5*time.Second + 400*time.Millisecond, + want: "02:05", + }, + { + d: 1*time.Second + 500*time.Millisecond, + want: "00:02", + }, + { + d: 2*time.Hour + 42*time.Minute + 12*time.Second, + want: "02:42:12", + }, + { + d: 24 * time.Hour, + want: "01-00:00:00", + }, + { + d: 24*time.Hour + 59*time.Minute + 59*time.Second, + want: "01-00:59:59", + }, + } + for _, tt := range tests { + t.Run(tt.d.String(), func(t *testing.T) { + if got := fmtEtimeDuration(tt.d); got != tt.want { + t.Errorf("fmtEtimeDuration() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go new file mode 100644 index 0000000..8496004 --- /dev/null +++ b/internal/cmd/root.go @@ -0,0 +1,171 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmd + +import ( + "bytes" + "fmt" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/google/gops/goprocess" +) + +const helpText = `gops is a tool to list and diagnose Go processes. + +Usage: + gops ... + gops # displays process info + gops help # displays this help message + +Commands: + stack Prints the stack trace. + gc Runs the garbage collector and blocks until successful. + setgc Sets the garbage collection target percentage. + memstats Prints the allocation and garbage collection stats. + version Prints the Go version used to build the program. + stats Prints runtime stats. + trace Runs the runtime tracer for 5 secs and launches "go tool trace". + pprof-heap Reads the heap profile and launches "go tool pprof". + pprof-cpu Reads the CPU profile and launches "go tool pprof". + +All commands require the agent running on the Go process. +"*" indicates the process is running the agent.` + +// TODO(jbd): add link that explains the use of agent. + +// Execute the root command. +func Execute() { + if len(os.Args) < 2 { + processes() + return + } + + cmd := os.Args[1] + + // See if it is a PID. + pid, err := strconv.Atoi(cmd) + if err == nil { + var period time.Duration + if len(os.Args) >= 3 { + period, err = time.ParseDuration(os.Args[2]) + if err != nil { + secs, _ := strconv.Atoi(os.Args[2]) + period = time.Duration(secs) * time.Second + } + } + processInfo(pid, period) + return + } + + if cmd == "help" { + usage("") + } + + if cmd == "tree" { + displayProcessTree() + return + } + + fn, ok := cmds[cmd] + if !ok { + usage("unknown subcommand") + } + if len(os.Args) < 3 { + usage("Missing PID or address.") + os.Exit(1) + } + addr, err := targetToAddr(os.Args[2]) + if err != nil { + fmt.Fprintf(os.Stderr, "Couldn't resolve addr or pid %v to TCPAddress: %v\n", os.Args[2], err) + os.Exit(1) + } + var params []string + if len(os.Args) > 3 { + params = append(params, os.Args[3:]...) + } + if err := fn(*addr, params); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} + +var develRe = regexp.MustCompile(`devel\s+\+\w+`) + +func processes() { + ps := goprocess.FindAll() + + var maxPID, maxPPID, maxExec, maxVersion int + for i, p := range ps { + ps[i].BuildVersion = shortenVersion(p.BuildVersion) + maxPID = max(maxPID, len(strconv.Itoa(p.PID))) + maxPPID = max(maxPPID, len(strconv.Itoa(p.PPID))) + maxExec = max(maxExec, len(p.Exec)) + maxVersion = max(maxVersion, len(ps[i].BuildVersion)) + + } + + for _, p := range ps { + buf := bytes.NewBuffer(nil) + pid := strconv.Itoa(p.PID) + fmt.Fprint(buf, pad(pid, maxPID)) + fmt.Fprint(buf, " ") + ppid := strconv.Itoa(p.PPID) + fmt.Fprint(buf, pad(ppid, maxPPID)) + fmt.Fprint(buf, " ") + fmt.Fprint(buf, pad(p.Exec, maxExec)) + if p.Agent { + fmt.Fprint(buf, "*") + } else { + fmt.Fprint(buf, " ") + } + fmt.Fprint(buf, " ") + fmt.Fprint(buf, pad(p.BuildVersion, maxVersion)) + fmt.Fprint(buf, " ") + fmt.Fprint(buf, p.Path) + fmt.Fprintln(buf) + buf.WriteTo(os.Stdout) + } +} + +func shortenVersion(v string) string { + if !strings.HasPrefix(v, "devel") { + return v + } + results := develRe.FindAllString(v, 1) + if len(results) == 0 { + return v + } + return results[0] +} + +func usage(msg string) { + // default exit code for the statement + exitCode := 0 + if msg != "" { + // founded an unexpected command + fmt.Printf("gops: %v\n", msg) + exitCode = 1 + } + fmt.Fprintf(os.Stderr, "%v\n", helpText) + os.Exit(exitCode) +} + +func pad(s string, total int) string { + if len(s) >= total { + return s + } + return s + strings.Repeat(" ", total-len(s)) +} + +func max(i, j int) int { + if i > j { + return i + } + return j +} diff --git a/internal/cmd/root_test.go b/internal/cmd/root_test.go new file mode 100644 index 0000000..c065ff5 --- /dev/null +++ b/internal/cmd/root_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmd + +import "testing" + +func Test_shortenVersion(t *testing.T) { + tests := []struct { + version string + want string + }{ + { + version: "go1.8.1.typealias", + want: "go1.8.1.typealias", + }, + { + version: "go1.9", + want: "go1.9", + }, + { + version: "go1.9rc", + want: "go1.9rc", + }, + { + version: "devel +990dac2723 Fri Jun 30 18:24:58 2017 +0000", + want: "devel +990dac2723", + }, + } + for _, tt := range tests { + t.Run(tt.version, func(t *testing.T) { + if got := shortenVersion(tt.version); got != tt.want { + t.Errorf("shortenVersion() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/cmd/shared.go b/internal/cmd/shared.go new file mode 100644 index 0000000..9f66fcf --- /dev/null +++ b/internal/cmd/shared.go @@ -0,0 +1,216 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmd + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/google/gops/internal" + "github.com/google/gops/signal" +) + +var cmds = map[string](func(addr net.TCPAddr, params []string) error){ + "stack": stackTrace, + "gc": gc, + "memstats": memStats, + "version": version, + "pprof-heap": pprofHeap, + "pprof-cpu": pprofCPU, + "stats": stats, + "trace": trace, + "setgc": setGC, +} + +func setGC(addr net.TCPAddr, params []string) error { + if len(params) != 1 { + return errors.New("missing gc percentage") + } + perc, err := strconv.ParseInt(params[0], 10, strconv.IntSize) + if err != nil { + return err + } + buf := make([]byte, binary.MaxVarintLen64) + binary.PutVarint(buf, perc) + return cmdWithPrint(addr, signal.SetGCPercent, buf...) +} + +func stackTrace(addr net.TCPAddr, _ []string) error { + return cmdWithPrint(addr, signal.StackTrace) +} + +func gc(addr net.TCPAddr, _ []string) error { + _, err := cmd(addr, signal.GC) + return err +} + +func memStats(addr net.TCPAddr, _ []string) error { + return cmdWithPrint(addr, signal.MemStats) +} + +func version(addr net.TCPAddr, _ []string) error { + return cmdWithPrint(addr, signal.Version) +} + +func pprofHeap(addr net.TCPAddr, _ []string) error { + return pprof(addr, signal.HeapProfile, "heap") +} + +func pprofCPU(addr net.TCPAddr, _ []string) error { + fmt.Println("Profiling CPU now, will take 30 secs...") + return pprof(addr, signal.CPUProfile, "cpu") +} + +func trace(addr net.TCPAddr, _ []string) error { + fmt.Println("Tracing now, will take 5 secs...") + out, err := cmd(addr, signal.Trace) + if err != nil { + return err + } + if len(out) == 0 { + return errors.New("nothing has traced") + } + tmpfile, err := ioutil.TempFile("", "trace") + if err != nil { + return err + } + if err := ioutil.WriteFile(tmpfile.Name(), out, 0); err != nil { + return err + } + fmt.Printf("Trace dump saved to: %s\n", tmpfile.Name()) + // If go tool chain not found, stopping here and keep trace file. + if _, err := exec.LookPath("go"); err != nil { + return nil + } + defer os.Remove(tmpfile.Name()) + cmd := exec.Command("go", "tool", "trace", tmpfile.Name()) + cmd.Env = os.Environ() + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func pprof(addr net.TCPAddr, p byte, prefix string) error { + tmpDumpFile, err := ioutil.TempFile("", prefix+"_profile") + if err != nil { + return err + } + { + out, err := cmd(addr, p) + if err != nil { + return err + } + if len(out) == 0 { + return errors.New("failed to read the profile") + } + if err := ioutil.WriteFile(tmpDumpFile.Name(), out, 0); err != nil { + return err + } + fmt.Printf("Profile dump saved to: %s\n", tmpDumpFile.Name()) + // If go tool chain not found, stopping here and keep dump file. + if _, err := exec.LookPath("go"); err != nil { + return nil + } + defer os.Remove(tmpDumpFile.Name()) + } + // Download running binary + tmpBinFile, err := ioutil.TempFile("", "binary") + if err != nil { + return err + } + { + out, err := cmd(addr, signal.BinaryDump) + if err != nil { + return fmt.Errorf("failed to read the binary: %v", err) + } + if len(out) == 0 { + return errors.New("failed to read the binary") + } + defer os.Remove(tmpBinFile.Name()) + if err := ioutil.WriteFile(tmpBinFile.Name(), out, 0); err != nil { + return err + } + } + fmt.Printf("Binary file saved to: %s\n", tmpBinFile.Name()) + cmd := exec.Command("go", "tool", "pprof", tmpBinFile.Name(), tmpDumpFile.Name()) + cmd.Env = os.Environ() + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func stats(addr net.TCPAddr, _ []string) error { + return cmdWithPrint(addr, signal.Stats) +} + +func cmdWithPrint(addr net.TCPAddr, c byte, params ...byte) error { + out, err := cmd(addr, c, params...) + if err != nil { + return err + } + fmt.Printf("%s", out) + return nil +} + +// targetToAddr tries to parse the target string, be it remote host:port +// or local process's PID. +func targetToAddr(target string) (*net.TCPAddr, error) { + if strings.Contains(target, ":") { + // addr host:port passed + var err error + addr, err := net.ResolveTCPAddr("tcp", target) + if err != nil { + return nil, fmt.Errorf("couldn't parse dst address: %v", err) + } + return addr, nil + } + // try to find port by pid then, connect to local + pid, err := strconv.Atoi(target) + if err != nil { + return nil, fmt.Errorf("couldn't parse PID: %v", err) + } + port, err := internal.GetPort(pid) + if err != nil { + return nil, fmt.Errorf("couldn't get port for PID %v: %v", pid, err) + } + addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:"+port) + return addr, nil +} + +func cmd(addr net.TCPAddr, c byte, params ...byte) ([]byte, error) { + conn, err := cmdLazy(addr, c, params...) + if err != nil { + return nil, fmt.Errorf("couldn't get port by PID: %v", err) + } + + all, err := ioutil.ReadAll(conn) + if err != nil { + return nil, err + } + return all, nil +} + +func cmdLazy(addr net.TCPAddr, c byte, params ...byte) (io.Reader, error) { + conn, err := net.DialTCP("tcp", nil, &addr) + if err != nil { + return nil, err + } + buf := []byte{c} + buf = append(buf, params...) + if _, err := conn.Write(buf); err != nil { + return nil, err + } + return conn, nil +} diff --git a/internal/cmd/tree.go b/internal/cmd/tree.go new file mode 100644 index 0000000..d814d9a --- /dev/null +++ b/internal/cmd/tree.go @@ -0,0 +1,56 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmd + +import ( + "fmt" + "sort" + "strconv" + + "github.com/google/gops/goprocess" + "github.com/xlab/treeprint" +) + +// displayProcessTree displays a tree of all the running Go processes. +func displayProcessTree() { + ps := goprocess.FindAll() + sort.Slice(ps, func(i, j int) bool { + return ps[i].PPID < ps[j].PPID + }) + pstree := make(map[int][]goprocess.P, len(ps)) + for _, p := range ps { + pstree[p.PPID] = append(pstree[p.PPID], p) + } + tree := treeprint.New() + tree.SetValue("...") + seen := map[int]bool{} + for _, p := range ps { + constructProcessTree(p.PPID, p, pstree, seen, tree) + } + fmt.Println(tree.String()) +} + +// constructProcessTree constructs the process tree in a depth-first fashion. +func constructProcessTree(ppid int, process goprocess.P, pstree map[int][]goprocess.P, seen map[int]bool, tree treeprint.Tree) { + + if seen[ppid] { + return + } + seen[ppid] = true + if ppid != process.PPID { + output := strconv.Itoa(ppid) + " (" + process.Exec + ")" + " {" + process.BuildVersion + "}" + if process.Agent { + tree = tree.AddMetaBranch("*", output) + } else { + tree = tree.AddBranch(output) + } + } else { + tree = tree.AddBranch(ppid) + } + for index := range pstree[ppid] { + process := pstree[ppid][index] + constructProcessTree(process.PID, process, pstree, seen, tree) + } +} diff --git a/main.go b/main.go index d6f9af7..b2cfd89 100644 --- a/main.go +++ b/main.go @@ -6,301 +6,9 @@ package main import ( - "bytes" - "fmt" - "log" - "math" - "os" - "regexp" - "sort" - "strconv" - "strings" - "time" - - "github.com/google/gops/goprocess" - "github.com/shirou/gopsutil/v3/process" - "github.com/xlab/treeprint" + "github.com/google/gops/internal/cmd" ) -const helpText = `gops is a tool to list and diagnose Go processes. - -Usage: - gops ... - gops # displays process info - gops help # displays this help message - -Commands: - stack Prints the stack trace. - gc Runs the garbage collector and blocks until successful. - setgc Sets the garbage collection target percentage. - memstats Prints the allocation and garbage collection stats. - version Prints the Go version used to build the program. - stats Prints runtime stats. - trace Runs the runtime tracer for 5 secs and launches "go tool trace". - pprof-heap Reads the heap profile and launches "go tool pprof". - pprof-cpu Reads the CPU profile and launches "go tool pprof". - -All commands require the agent running on the Go process. -"*" indicates the process is running the agent.` - -// TODO(jbd): add link that explains the use of agent. - func main() { - if len(os.Args) < 2 { - processes() - return - } - - cmd := os.Args[1] - - // See if it is a PID. - pid, err := strconv.Atoi(cmd) - if err == nil { - var period time.Duration - if len(os.Args) >= 3 { - period, err = time.ParseDuration(os.Args[2]) - if err != nil { - secs, _ := strconv.Atoi(os.Args[2]) - period = time.Duration(secs) * time.Second - } - } - processInfo(pid, period) - return - } - - if cmd == "help" { - usage("") - } - - if cmd == "tree" { - displayProcessTree() - return - } - - fn, ok := cmds[cmd] - if !ok { - usage("unknown subcommand") - } - if len(os.Args) < 3 { - usage("Missing PID or address.") - os.Exit(1) - } - addr, err := targetToAddr(os.Args[2]) - if err != nil { - fmt.Fprintf(os.Stderr, "Couldn't resolve addr or pid %v to TCPAddress: %v\n", os.Args[2], err) - os.Exit(1) - } - var params []string - if len(os.Args) > 3 { - params = append(params, os.Args[3:]...) - } - if err := fn(*addr, params); err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - os.Exit(1) - } -} - -func processes() { - ps := goprocess.FindAll() - - var maxPID, maxPPID, maxExec, maxVersion int - for i, p := range ps { - ps[i].BuildVersion = shortenVersion(p.BuildVersion) - maxPID = max(maxPID, len(strconv.Itoa(p.PID))) - maxPPID = max(maxPPID, len(strconv.Itoa(p.PPID))) - maxExec = max(maxExec, len(p.Exec)) - maxVersion = max(maxVersion, len(ps[i].BuildVersion)) - - } - - for _, p := range ps { - buf := bytes.NewBuffer(nil) - pid := strconv.Itoa(p.PID) - fmt.Fprint(buf, pad(pid, maxPID)) - fmt.Fprint(buf, " ") - ppid := strconv.Itoa(p.PPID) - fmt.Fprint(buf, pad(ppid, maxPPID)) - fmt.Fprint(buf, " ") - fmt.Fprint(buf, pad(p.Exec, maxExec)) - if p.Agent { - fmt.Fprint(buf, "*") - } else { - fmt.Fprint(buf, " ") - } - fmt.Fprint(buf, " ") - fmt.Fprint(buf, pad(p.BuildVersion, maxVersion)) - fmt.Fprint(buf, " ") - fmt.Fprint(buf, p.Path) - fmt.Fprintln(buf) - buf.WriteTo(os.Stdout) - } -} - -func processInfo(pid int, period time.Duration) { - if period < 0 { - log.Fatalf("Cannot determine CPU usage for negative duration %v", period) - } - p, err := process.NewProcess(int32(pid)) - if err != nil { - log.Fatalf("Cannot read process info: %v", err) - } - if v, err := p.Parent(); err == nil { - fmt.Printf("parent PID:\t%v\n", v.Pid) - } - if v, err := p.NumThreads(); err == nil { - fmt.Printf("threads:\t%v\n", v) - } - if v, err := p.MemoryPercent(); err == nil { - fmt.Printf("memory usage:\t%.3f%%\n", v) - } - if v, err := p.CPUPercent(); err == nil { - fmt.Printf("cpu usage:\t%.3f%%\n", v) - } - if period > 0 { - if v, err := cpuPercentWithinTime(p, period); err == nil { - fmt.Printf("cpu usage (%v):\t%.3f%%\n", period, v) - } - } - if v, err := p.Username(); err == nil { - fmt.Printf("username:\t%v\n", v) - } - if v, err := p.Cmdline(); err == nil { - fmt.Printf("cmd+args:\t%v\n", v) - } - if v, err := elapsedTime(p); err == nil { - fmt.Printf("elapsed time:\t%v\n", v) - } - if v, err := p.Connections(); err == nil { - if len(v) > 0 { - for _, conn := range v { - fmt.Printf("local/remote:\t%v:%v <-> %v:%v (%v)\n", - conn.Laddr.IP, conn.Laddr.Port, conn.Raddr.IP, conn.Raddr.Port, conn.Status) - } - } - } -} - -// cpuPercentWithinTime return how many percent of the CPU time this process uses within given time duration -func cpuPercentWithinTime(p *process.Process, t time.Duration) (float64, error) { - cput, err := p.Times() - if err != nil { - return 0, err - } - time.Sleep(t) - cput2, err := p.Times() - if err != nil { - return 0, err - } - return 100 * (cput2.Total() - cput.Total()) / t.Seconds(), nil -} - -// elapsedTime shows the elapsed time of the process indicating how long the -// process has been running for. -func elapsedTime(p *process.Process) (string, error) { - crtTime, err := p.CreateTime() - if err != nil { - return "", err - } - etime := time.Since(time.Unix(crtTime/1000, 0)) - return fmtEtimeDuration(etime), nil -} - -// displayProcessTree displays a tree of all the running Go processes. -func displayProcessTree() { - ps := goprocess.FindAll() - sort.Slice(ps, func(i, j int) bool { - return ps[i].PPID < ps[j].PPID - }) - pstree := make(map[int][]goprocess.P, len(ps)) - for _, p := range ps { - pstree[p.PPID] = append(pstree[p.PPID], p) - } - tree := treeprint.New() - tree.SetValue("...") - seen := map[int]bool{} - for _, p := range ps { - constructProcessTree(p.PPID, p, pstree, seen, tree) - } - fmt.Print(tree.String()) -} - -// constructProcessTree constructs the process tree in a depth-first fashion. -func constructProcessTree(ppid int, process goprocess.P, pstree map[int][]goprocess.P, seen map[int]bool, tree treeprint.Tree) { - if seen[ppid] { - return - } - seen[ppid] = true - if ppid != process.PPID { - output := strconv.Itoa(ppid) + " (" + process.Exec + ")" + " {" + process.BuildVersion + "}" - if process.Agent { - tree = tree.AddMetaBranch("*", output) - } else { - tree = tree.AddBranch(output) - } - } else { - tree = tree.AddBranch(ppid) - } - for index := range pstree[ppid] { - process := pstree[ppid][index] - constructProcessTree(process.PID, process, pstree, seen, tree) - } -} - -var develRe = regexp.MustCompile(`devel\s+\+\w+`) - -func shortenVersion(v string) string { - if !strings.HasPrefix(v, "devel") { - return v - } - results := develRe.FindAllString(v, 1) - if len(results) == 0 { - return v - } - return results[0] -} - -func usage(msg string) { - // default exit code for the statement - exitCode := 0 - if msg != "" { - // founded an unexpected command - fmt.Printf("gops: %v\n", msg) - exitCode = 1 - } - fmt.Fprintf(os.Stderr, "%v\n", helpText) - os.Exit(exitCode) -} - -func pad(s string, total int) string { - if len(s) >= total { - return s - } - return s + strings.Repeat(" ", total-len(s)) -} - -func max(i, j int) int { - if i > j { - return i - } - return j -} - -// fmtEtimeDuration formats etime's duration based on ps' format: -// [[DD-]hh:]mm:ss -// format specification: http://linuxcommand.org/lc3_man_pages/ps1.html -func fmtEtimeDuration(d time.Duration) string { - days := d / (24 * time.Hour) - hours := d % (24 * time.Hour) - minutes := hours % time.Hour - seconds := math.Mod(minutes.Seconds(), 60) - var b strings.Builder - if days > 0 { - fmt.Fprintf(&b, "%02d-", days) - } - if days > 0 || hours/time.Hour > 0 { - fmt.Fprintf(&b, "%02d:", hours/time.Hour) - } - fmt.Fprintf(&b, "%02d:", minutes/time.Minute) - fmt.Fprintf(&b, "%02.0f", seconds) - return b.String() + cmd.Execute() } diff --git a/main_test.go b/main_test.go index c3d431d..175eb2a 100644 --- a/main_test.go +++ b/main_test.go @@ -3,78 +3,3 @@ // license that can be found in the LICENSE file. package main - -import ( - "testing" - "time" -) - -func Test_shortenVersion(t *testing.T) { - tests := []struct { - version string - want string - }{ - { - version: "go1.8.1.typealias", - want: "go1.8.1.typealias", - }, - { - version: "go1.9", - want: "go1.9", - }, - { - version: "go1.9rc", - want: "go1.9rc", - }, - { - version: "devel +990dac2723 Fri Jun 30 18:24:58 2017 +0000", - want: "devel +990dac2723", - }, - } - for _, tt := range tests { - t.Run(tt.version, func(t *testing.T) { - if got := shortenVersion(tt.version); got != tt.want { - t.Errorf("shortenVersion() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_fmtEtimeDuration(t *testing.T) { - tests := []struct { - d time.Duration - want string - }{ - { - want: "00:00", - }, - - { - d: 2*time.Minute + 5*time.Second + 400*time.Millisecond, - want: "02:05", - }, - { - d: 1*time.Second + 500*time.Millisecond, - want: "00:02", - }, - { - d: 2*time.Hour + 42*time.Minute + 12*time.Second, - want: "02:42:12", - }, - { - d: 24 * time.Hour, - want: "01-00:00:00", - }, - { - d: 24*time.Hour + 59*time.Minute + 59*time.Second, - want: "01-00:59:59", - }, - } - for _, tt := range tests { - t.Run(tt.d.String(), func(t *testing.T) { - if got := fmtEtimeDuration(tt.d); got != tt.want { - t.Errorf("fmtEtimeDuration() = %v, want %v", got, tt.want) - } - }) - } -}