1
0
mirror of https://github.com/google/gops.git synced 2024-11-19 20:31:58 +02:00

Migrate gops to use spf13/cobra for command execution

Manual command setup was starting to show some strain in
terms of documentation and also adding new features
(regardless of what the protocol supports currently).

This new setup aims to make it easier to add new
documentation and functionality.

In comparison to the previous version, this increased the
binary size by 2.4M.
```
6.4M gops
4.0M gops_master
```
This commit is contained in:
Glib Smaga 2022-06-28 09:18:41 -07:00 committed by Tobias Klauser
parent 3655f623db
commit 9069aa4d19
8 changed files with 232 additions and 99 deletions

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.13
require (
github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19
github.com/shirou/gopsutil/v3 v3.22.4
github.com/spf13/cobra v1.4.0
github.com/xlab/treeprint v1.1.0
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
rsc.io/goversion v1.2.0

9
go.sum
View File

@ -1,3 +1,4 @@
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
@ -5,6 +6,8 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19 h1:WjT3fLi9n8YWh/Ih8Q1LHAPsTqGddPcHqscN+PJ3i68=
github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19/go.mod h1:hY+WOq6m2FpbvyrI93sMaypsttvaIL5nhVR92dTMUcQ=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
@ -13,8 +16,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v3 v3.22.4 h1:srAQaiX6jX/cYL6q29aE0m8lOskT9CurZ9N61YR3yoI=
github.com/shirou/gopsutil/v3 v3.22.4/go.mod h1:D01hZJ4pVHPpCTZ3m3T2+wDF2YAGfd+H4ifUguaQzHM=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
@ -36,6 +44,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=

View File

@ -8,12 +8,44 @@ import (
"fmt"
"log"
"math"
"strconv"
"strings"
"time"
"github.com/shirou/gopsutil/v3/process"
"github.com/spf13/cobra"
)
// ProcessCommand displays information about a Go process.
func ProcessCommand() *cobra.Command {
return &cobra.Command{
Use: "process",
Aliases: []string{"pid", "proc"},
Short: "Prints information about a Go process.",
RunE: func(cmd *cobra.Command, args []string) error {
ProcessInfo(args)
return nil
},
}
}
// ProcessInfo takes arguments starting with pid|:addr and grabs all kinds of
// useful Go process information.
func ProcessInfo(args []string) {
pid, err := strconv.Atoi(args[0])
var period time.Duration
if len(args) >= 2 {
period, err = time.ParseDuration(args[1])
if err != nil {
secs, _ := strconv.Atoi(args[1])
period = time.Duration(secs) * time.Second
}
}
processInfo(pid, period)
}
func processInfo(pid int, period time.Duration) {
if period < 0 {
log.Fatalf("Cannot determine CPU usage for negative duration %v", period)

View File

@ -11,87 +11,23 @@ import (
"regexp"
"strconv"
"strings"
"time"
"github.com/google/gops/goprocess"
"github.com/spf13/cobra"
)
const helpText = `gops is a tool to list and diagnose Go processes.
Usage:
gops <cmd> <pid|addr> ...
// NewRoot command.
func NewRoot() *cobra.Command {
return &cobra.Command{
Use: "gops",
Short: "gops is a tool to list and diagnose Go processes.",
Example: ` 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 {
gops help # displays this help message`,
// TODO(jbd): add link that explains the use of agent.
Run: func(cmd *cobra.Command, args []string) {
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)
},
}
}
@ -144,18 +80,6 @@ func shortenVersion(v string) string {
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

View File

@ -18,18 +18,109 @@ import (
"github.com/google/gops/internal"
"github.com/google/gops/signal"
"github.com/spf13/cobra"
)
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,
// AgentCommands is a bridge between the legacy multiplexing to commands, and
// full migration to Cobra for each command.
//
// The code is already nicely structured with one function per command so it
// seemed cleaner to combine them all together here and "generate" cobra
// commands as just thin wrappers, rather through individual constructors.
func AgentCommands() []*cobra.Command {
var res []*cobra.Command
var cmds = []legacyCommand{
{
name: "stack",
short: "Prints the stack trace.",
fn: stackTrace,
},
{
name: "gc",
short: "Runs the garbage collector and blocks until successful.",
fn: gc,
},
{
name: "setgc",
short: "Sets the garbage collection target percentage.",
fn: setGC,
},
{
name: "memstats",
short: "Prints the allocation and garbage collection stats.",
fn: memStats,
},
{
name: "stats",
short: "Prints runtime stats.",
fn: stats,
},
{
name: "trace",
short: "Runs the runtime tracer for 5 secs and launches \"go tool trace\".",
fn: trace,
},
{
name: "pprof-heap",
short: "Reads the heap profile and launches \"go tool pprof\".",
fn: pprofHeap,
},
{
name: "pprof-cpu",
short: "Reads the CPU profile and launches \"go tool pprof\".",
fn: pprofCPU,
},
{
name: "version",
short: "Prints the Go version used to build the program.",
fn: version,
},
}
for _, c := range cmds {
c := c
res = append(res, &cobra.Command{
Use: fmt.Sprintf("%s <pid|addr>", c.name),
Short: c.short,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("missing PID or address")
}
addr, err := targetToAddr(args[0])
if err != nil {
return fmt.Errorf(
"couldn't resolve addr or pid %v to TCPAddress: %v\n", args[0], err,
)
}
var params []string
if len(args) > 1 {
params = append(params, args[1:]...)
}
if err := c.fn(*addr, params); err != nil {
return err
}
return nil
},
// errors get double printed otherwise
SilenceUsage: true,
SilenceErrors: true,
})
}
return res
}
type legacyCommand struct {
name string
short string
fn func(addr net.TCPAddr, params []string) error
}
func setGC(addr net.TCPAddr, params []string) error {

View File

@ -0,0 +1,39 @@
// 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"
"strings"
"testing"
"github.com/spf13/cobra"
)
func TestCommandPresence(t *testing.T) {
cmd := &cobra.Command{Use: "gops"}
cmd.AddCommand(AgentCommands()...)
var out bytes.Buffer
cmd.SetOut(&out)
cmd.SetArgs([]string{"--help"})
if err := cmd.Execute(); err != nil {
t.Error(err)
}
// basic check to make sure all the legacy commands have been ported over
// it doesn't test they are correctly _implemented_, just that they are not
// missing.
wants := []string{
"completion", "gc", "memstats", "pprof-cpu", "pprof-heap", "setgc",
"stack", "stats", "trace", "version",
}
outs := out.String()
for _, want := range wants {
if !strings.Contains(outs, want) {
t.Errorf("%q command not found in help", want)
}
}
}

View File

@ -10,9 +10,21 @@ import (
"strconv"
"github.com/google/gops/goprocess"
"github.com/spf13/cobra"
"github.com/xlab/treeprint"
)
// TreeCommand displays a process tree.
func TreeCommand() *cobra.Command {
return &cobra.Command{
Use: "tree",
Short: "Display parent-child tree for Go processes.",
Run: func(cmd *cobra.Command, args []string) {
displayProcessTree()
},
}
}
// displayProcessTree displays a tree of all the running Go processes.
func displayProcessTree() {
ps := goprocess.FindAll()

27
main.go
View File

@ -6,9 +6,34 @@
package main
import (
"log"
"os"
"strconv"
"github.com/google/gops/internal/cmd"
)
func main() {
cmd.Execute()
var root = cmd.NewRoot()
root.AddCommand(cmd.ProcessCommand())
root.AddCommand(cmd.TreeCommand())
root.AddCommand(cmd.AgentCommands()...)
// Legacy support for `gops <pid>` command.
//
// When the second argument is provided as int as opposed to a sub-command
// (like proc, version, etc), gops command effectively shortcuts that
// to `gops process <pid>`.
if len(os.Args) > 1 {
// See second argument appears to be a pid rather than a subcommand
_, err := strconv.Atoi(os.Args[1])
if err == nil {
cmd.ProcessInfo(os.Args[1:]) // shift off the command name
return
}
}
if err := root.Execute(); err != nil {
log.Fatal(err)
}
}