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:
parent
9f8280461a
commit
024cce5849
211
cmd.go
211
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
|
||||
}
|
||||
|
104
internal/cmd/process.go
Normal file
104
internal/cmd/process.go
Normal 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()
|
||||
}
|
49
internal/cmd/process_test.go
Normal file
49
internal/cmd/process_test.go
Normal 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
171
internal/cmd/root.go
Normal 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
38
internal/cmd/root_test.go
Normal 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
216
internal/cmd/shared.go
Normal 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
56
internal/cmd/tree.go
Normal 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
296
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 <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()
|
||||
}
|
||||
|
75
main_test.go
75
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user