1
0
mirror of https://github.com/google/gops.git synced 2025-02-01 19:14:30 +02:00

internal/cmd: Move code here from the main package

This commit is contained in:
Glib Smaga 2022-06-28 09:14:59 -07:00 committed by Tobias Klauser
parent 9f8280461a
commit 024cce5849
9 changed files with 636 additions and 580 deletions

211
cmd.go
View File

@ -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
}

104
internal/cmd/process.go Normal file
View File

@ -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()
}

View File

@ -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)
}
})
}
}

171
internal/cmd/root.go Normal file
View File

@ -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 <cmd> <pid|addr> ...
gops <pid> # 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
}

38
internal/cmd/root_test.go Normal file
View File

@ -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)
}
})
}
}

216
internal/cmd/shared.go Normal file
View File

@ -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
}

56
internal/cmd/tree.go Normal file
View File

@ -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)
}
}

296
main.go
View File

@ -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 <cmd> <pid|addr> ...
gops <pid> # 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()
}

View File

@ -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)
}
})
}
}