1
0
mirror of https://github.com/google/gops.git synced 2024-11-24 08:22:25 +02:00
gops/main.go
Tobias Klauser bf851ab3df Sort tree output by PPID
Otherwise the processes might appear in arbitrary order and
disassociated from their parent process. Also avoid printing a stray
newline after the tree.

Before:

```
...
├── 2493
│   └── 4952 (gops) {go1.18.1}
├── 1
│   └── 117 (com.docker.vmnetd) {go1.17.5}
├── 4405
│   └── 4416 (gopls) {go1.18.2}
└── 4404

```

After:

```
...
├── 1
│   └── 117 (com.docker.vmnetd) {go1.17.5}
├── 2493
│   └── 4945 (gops) {go1.18.3}
└── 4404
    └── 4405 (gopls) {go1.18.2}
        └── 4416 (gopls) {go1.18.2}
```
2022-06-24 22:54:34 +02:00

305 lines
7.4 KiB
Go

// Copyright 2016 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.
// Program gops is a tool to list currently running Go processes.
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"
)
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 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()
}