Repository: google/gops Branch: master Commit: a2d8f7790eac Files: 29 Total size: 54.5 KB Directory structure: gitextract_m6aexidg/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── agent/ │ ├── agent.go │ ├── agent_test.go │ ├── sockopt_reuseport.go │ └── sockopt_unsupported.go ├── cmd.go ├── examples/ │ └── hello/ │ └── main.go ├── go.mod ├── go.sum ├── goprocess/ │ ├── goprocess.go │ ├── goprocess_1.18.go │ ├── goprocess_lt1.18.go │ └── goprocess_test.go ├── internal/ │ ├── cmd/ │ │ ├── process.go │ │ ├── process_test.go │ │ ├── root.go │ │ ├── root_test.go │ │ ├── shared.go │ │ ├── shared_test.go │ │ └── tree.go │ ├── internal.go │ └── internal_test.go ├── main.go ├── main_test.go └── signal/ └── signal.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: gomod directory: / schedule: interval: daily open-pull-requests-limit: 1 rebase-strategy: disabled - package-ecosystem: github-actions directory: / schedule: interval: daily open-pull-requests-limit: 5 rebase-strategy: disabled ================================================ FILE: .github/workflows/test.yml ================================================ name: Tests on: push: branches: - master pull_request: branches: - master jobs: test: strategy: matrix: # Minimum supported version (1.18) and the latest two go-version: ['1.18', '1.22', '1.23'] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v4 - name: Check formatting if: matrix.go-version == '1.23' && matrix.platform == 'ubuntu-latest' run: diff -u <(echo -n) <(go fmt $(go list ./...)) - name: Run unit tests run: go test -race -v ./... ================================================ FILE: .gitignore ================================================ gops ================================================ FILE: LICENSE ================================================ Copyright (c) 2016 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # gops [![GitHub Action Status](https://github.com/google/gops/workflows/Tests/badge.svg)](https://github.com/google/gops/actions?query=workflow%3ATests) [![GoDoc](https://godoc.org/github.com/google/gops?status.svg)](https://godoc.org/github.com/google/gops) gops is a command to list and diagnose Go processes currently running on your system. ```sh $ gops 983 980 uplink-soecks go1.9 /usr/local/bin/uplink-soecks 52697 52695 gops go1.10 /Users/jbd/bin/gops 4132 4130 foops * go1.9 /Users/jbd/bin/foops 51130 51128 gocode go1.9.2 /Users/jbd/bin/gocode ``` ## Installation To install the latest version of gops: ```sh $ go get github.com/google/gops ``` or ```sh $ go install github.com/google/gops@latest ``` To install a specific gops version, for example v0.3.19: ```sh $ go install github.com/google/gops@v0.3.19 ``` ## Diagnostics For processes that start the diagnostics agent, gops can report additional information such as the current stack trace, Go version, memory stats, etc. In order to start the diagnostics agent, see the [hello example](https://github.com/google/gops/blob/master/examples/hello/main.go). ``` go package main import ( "log" "time" "github.com/google/gops/agent" ) func main() { if err := agent.Listen(agent.Options{}); err != nil { log.Fatal(err) } time.Sleep(time.Hour) } ``` Otherwise, you could set `GOPS_CONFIG_DIR` environment variables to assign your config dir. Default, gops will use the current user's home directory(AppData on windows). ### Manual It is possible to use gops tool both in local and remote mode. Local mode requires that you start the target binary as the same user that runs gops binary. To use gops in a remote mode you need to know target's agent address. In Local mode use process's PID as a target; in Remote mode target is a `host:port` combination. #### Listing all processes running locally To print all go processes, run `gops` without arguments: ```sh $ gops 983 980 uplink-soecks go1.9 /usr/local/bin/uplink-soecks 52697 52695 gops go1.10 /Users/jbd/bin/gops 4132 4130 foops * go1.9 /Users/jbd/bin/foops 51130 51128 gocode go1.9.2 /Users/jbd/bin/gocode ``` The output displays: * PID * PPID * Name of the program * Go version used to build the program * Location of the associated program Note that processes running the agent are marked with `*` next to the PID (e.g. `4132*`). #### $ gops \ [duration] To report more information about a process, run `gops` followed by a PID: ```sh $ gops parent PID: 5985 threads: 27 memory usage: 0.199% cpu usage: 0.139% username: jbd cmd+args: /Applications/Splice.app/Contents/Resources/Splice Helper.app/Contents/MacOS/Splice Helper -pid 5985 local/remote: 127.0.0.1:56765 <-> :0 (LISTEN) local/remote: 127.0.0.1:56765 <-> 127.0.0.1:50955 (ESTABLISHED) local/remote: 100.76.175.164:52353 <-> 54.241.191.232:443 (ESTABLISHED) ``` If an optional duration is specified in the format as expected by [`time.ParseDuration`](https://golang.org/pkg/time/#ParseDuration), the CPU usage for the given time period is reported in addition: ```sh $ gops 2s parent PID: 5985 threads: 27 memory usage: 0.199% cpu usage: 0.139% cpu usage (2s): 0.271% username: jbd cmd+args: /Applications/Splice.app/Contents/Resources/Splice Helper.app/Contents/MacOS/Splice Helper -pid 5985 local/remote: 127.0.0.1:56765 <-> :0 (LISTEN) local/remote: 127.0.0.1:56765 <-> 127.0.0.1:50955 (ESTABLISHED) local/remote: 100.76.175.164:52353 <-> 54.241.191.232:443 (ESTABLISHED) ``` #### $ gops tree To display a process tree with all the running Go processes, run the following command: ```sh $ gops tree ... ├── 1 │   └── 13962 [gocode] {go1.9} ├── 557 │   └── 635 [com.docker.supervisor] {go1.9.2} │   └── 638 [com.docker.driver.amd64-linux] {go1.9.2} └── 13744 └── 67243 [gops] {go1.10} ``` #### $ gops stack (\|\) In order to print the current stack trace from a target program, run the following command: ```sh $ gops stack (|) gops stack 85709 goroutine 8 [running]: runtime/pprof.writeGoroutineStacks(0x13c7bc0, 0xc42000e008, 0xc420ec8520, 0xc420ec8520) /Users/jbd/go/src/runtime/pprof/pprof.go:603 +0x79 runtime/pprof.writeGoroutine(0x13c7bc0, 0xc42000e008, 0x2, 0xc428f1c048, 0xc420ec8608) /Users/jbd/go/src/runtime/pprof/pprof.go:592 +0x44 runtime/pprof.(*Profile).WriteTo(0x13eeda0, 0x13c7bc0, 0xc42000e008, 0x2, 0xc42000e008, 0x0) /Users/jbd/go/src/runtime/pprof/pprof.go:302 +0x3b5 github.com/google/gops/agent.handle(0x13cd560, 0xc42000e008, 0xc420186000, 0x1, 0x1, 0x0, 0x0) /Users/jbd/src/github.com/google/gops/agent/agent.go:150 +0x1b3 github.com/google/gops/agent.listen() /Users/jbd/src/github.com/google/gops/agent/agent.go:113 +0x2b2 created by github.com/google/gops/agent.Listen /Users/jbd/src/github.com/google/gops/agent/agent.go:94 +0x480 # ... ``` #### $ gops memstats (\|\) To print the current memory stats, run the following command: ```sh $ gops memstats (|) ``` #### $ gops gc (\|\) If you want to force run garbage collection on the target program, run `gc`. It will block until the GC is completed. #### $ gops setgc (\|\) Sets the garbage collection target to a certain percentage. The following command sets it to 10%: ``` sh $ gops setgc (|) 10 ``` The following command turns off the garbage collector: ```sh $ gops setgc (|) off ``` #### $ gops version (\|\) gops reports the Go version the target program is built with, if you run the following: ```sh $ gops version (|) devel +6a3c6c0 Sat Jan 14 05:57:07 2017 +0000 ``` #### $ gops stats (\|\) To print the runtime statistics such as number of goroutines and `GOMAXPROCS`. #### Profiling ##### Pprof gops supports CPU and heap pprof profiles. After reading either heap or CPU profile, it shells out to the `go tool pprof` and let you interactively examine the profiles. To enter the CPU profile, run: ```sh $ gops pprof-cpu (|) ``` To enter the heap profile, run: ```sh $ gops pprof-heap (|) ``` ##### Execution trace gops allows you to start the runtime tracer for 5 seconds and examine the results. ```sh $ gops trace (|) ``` ================================================ FILE: agent/agent.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. // Package agent provides hooks programs can register to retrieve // diagnostics data by using gops. package agent import ( "bufio" "context" "encoding/binary" "errors" "fmt" "io" "net" "os" gosignal "os/signal" "path/filepath" "runtime" "runtime/debug" "runtime/pprof" "runtime/trace" "strconv" "sync" "syscall" "time" "github.com/google/gops/internal" "github.com/google/gops/signal" ) const defaultAddr = "127.0.0.1:0" var ( mu sync.Mutex portfile string listener net.Listener units = []string{" bytes", "KB", "MB", "GB", "TB", "PB"} ) // Options allows configuring the started agent. type Options struct { // Addr is the host:port the agent will be listening at. // Optional. Addr string // ConfigDir is the directory to store the configuration file, // PID of the gops process, filename, port as well as content. // Optional. ConfigDir string // ShutdownCleanup automatically cleans up resources if the // running process receives an interrupt. Otherwise, users // can call Close before shutting down. // Optional. ShutdownCleanup bool // ReuseSocketAddrAndPort determines whether the SO_REUSEADDR and // SO_REUSEPORT socket options should be set on the listening socket of // the agent. This option is only effective on unix-like OSes and if // Addr is set to a fixed host:port. // Optional. ReuseSocketAddrAndPort bool } // Listen starts the gops agent on a host process. Once agent started, users // can use the advanced gops features. The agent will listen to Interrupt // signals and exit the process, if you need to perform further work on the // Interrupt signal use the options parameter to configure the agent // accordingly. // // Note: The agent exposes an endpoint via a TCP connection that can be used by // any program on the system. Review your security requirements before starting // the agent. func Listen(opts Options) error { mu.Lock() defer mu.Unlock() if listener != nil { return fmt.Errorf("gops: agent already listening at: %v", listener.Addr()) } addr := opts.Addr if addr == "" { addr = defaultAddr } var lc net.ListenConfig if opts.ReuseSocketAddrAndPort { lc.Control = setReuseAddrAndPortSockopts } var err error listener, err = lc.Listen(context.Background(), "tcp", addr) if err != nil { return err } port := listener.Addr().(*net.TCPAddr).Port err = saveConfig(opts, port) if err != nil { // ignore and work in remote mode only if !errors.Is(err, syscall.EROFS) && !errors.Is(err, syscall.EPERM) { return err } } if opts.ShutdownCleanup { gracefulShutdown() } go listen(listener) return nil } func listen(l net.Listener) { buf := make([]byte, 1) for { fd, err := l.Accept() if err != nil { if !errors.Is(err, net.ErrClosed) { fmt.Fprintf(os.Stderr, "gops: %v\n", err) } if netErr, ok := err.(net.Error); ok && !netErr.Temporary() { break } continue } if _, err := fd.Read(buf); err != nil { fmt.Fprintf(os.Stderr, "gops: %v\n", err) continue } if err := handle(fd, buf); err != nil { fmt.Fprintf(os.Stderr, "gops: %v\n", err) continue } fd.Close() } } func saveConfig(opts Options, port int) error { gopsdir := opts.ConfigDir if gopsdir == "" { cfgDir, err := internal.ConfigDir() if err != nil { return err } gopsdir = cfgDir } err := os.MkdirAll(gopsdir, os.ModePerm) if err != nil { return err } portfile = filepath.Join(gopsdir, strconv.Itoa(os.Getpid())) return os.WriteFile(portfile, []byte(strconv.Itoa(port)), os.ModePerm) } func gracefulShutdown() { c := make(chan os.Signal, 1) gosignal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) go func() { // cleanup the socket on shutdown. sig := <-c Close() ret := 1 if sig == syscall.SIGTERM { ret = 0 } os.Exit(ret) }() } // Close closes the agent, removing temporary files and closing the TCP listener. // If no agent is listening, Close does nothing. func Close() { mu.Lock() defer mu.Unlock() if portfile != "" { os.Remove(portfile) portfile = "" } if listener != nil { listener.Close() listener = nil } } func formatBytes(val uint64) string { var i int var target uint64 for i = range units { target = 1 << uint(10*(i+1)) if val < target { break } } if i > 0 { return fmt.Sprintf("%0.2f%s (%d bytes)", float64(val)/(float64(target)/1024), units[i], val) } return fmt.Sprintf("%d bytes", val) } func handle(conn io.ReadWriter, msg []byte) error { switch msg[0] { case signal.StackTrace: return pprof.Lookup("goroutine").WriteTo(conn, 2) case signal.GC: runtime.GC() _, err := conn.Write([]byte("ok")) return err case signal.MemStats: var s runtime.MemStats runtime.ReadMemStats(&s) fmt.Fprintf(conn, "alloc: %v\n", formatBytes(s.Alloc)) fmt.Fprintf(conn, "total-alloc: %v\n", formatBytes(s.TotalAlloc)) fmt.Fprintf(conn, "sys: %v\n", formatBytes(s.Sys)) fmt.Fprintf(conn, "lookups: %v\n", s.Lookups) fmt.Fprintf(conn, "mallocs: %v\n", s.Mallocs) fmt.Fprintf(conn, "frees: %v\n", s.Frees) fmt.Fprintf(conn, "heap-alloc: %v\n", formatBytes(s.HeapAlloc)) fmt.Fprintf(conn, "heap-sys: %v\n", formatBytes(s.HeapSys)) fmt.Fprintf(conn, "heap-idle: %v\n", formatBytes(s.HeapIdle)) fmt.Fprintf(conn, "heap-in-use: %v\n", formatBytes(s.HeapInuse)) fmt.Fprintf(conn, "heap-released: %v\n", formatBytes(s.HeapReleased)) fmt.Fprintf(conn, "heap-objects: %v\n", s.HeapObjects) fmt.Fprintf(conn, "stack-in-use: %v\n", formatBytes(s.StackInuse)) fmt.Fprintf(conn, "stack-sys: %v\n", formatBytes(s.StackSys)) fmt.Fprintf(conn, "mspan-in-use: %v\n", formatBytes(s.MSpanInuse)) fmt.Fprintf(conn, "mspan-sys: %v\n", formatBytes(s.MSpanSys)) fmt.Fprintf(conn, "mcache-in-use: %v\n", formatBytes(s.MCacheInuse)) fmt.Fprintf(conn, "mcache-sys: %v\n", formatBytes(s.MCacheSys)) fmt.Fprintf(conn, "buck-hash-sys: %v\n", formatBytes(s.BuckHashSys)) fmt.Fprintf(conn, "other-sys: %v\n", formatBytes(s.OtherSys)) fmt.Fprintf(conn, "gc-sys: %v\n", formatBytes(s.GCSys)) fmt.Fprintf(conn, "next-gc: when heap-alloc >= %v\n", formatBytes(s.NextGC)) lastGC := "-" if s.LastGC != 0 { lastGC = fmt.Sprint(time.Unix(0, int64(s.LastGC))) } fmt.Fprintf(conn, "last-gc: %v\n", lastGC) fmt.Fprintf(conn, "gc-pause-total: %v\n", time.Duration(s.PauseTotalNs)) fmt.Fprintf(conn, "gc-pause: %v\n", s.PauseNs[(s.NumGC+255)%256]) fmt.Fprintf(conn, "gc-pause-end: %v\n", s.PauseEnd[(s.NumGC+255)%256]) fmt.Fprintf(conn, "num-gc: %v\n", s.NumGC) fmt.Fprintf(conn, "num-forced-gc: %v\n", s.NumForcedGC) fmt.Fprintf(conn, "gc-cpu-fraction: %v\n", s.GCCPUFraction) fmt.Fprintf(conn, "enable-gc: %v\n", s.EnableGC) fmt.Fprintf(conn, "debug-gc: %v\n", s.DebugGC) case signal.Version: fmt.Fprintf(conn, "%v\n", runtime.Version()) case signal.HeapProfile: return pprof.WriteHeapProfile(conn) case signal.CPUProfile: if err := pprof.StartCPUProfile(conn); err != nil { return err } time.Sleep(30 * time.Second) pprof.StopCPUProfile() case signal.Stats: fmt.Fprintf(conn, "goroutines: %v\n", runtime.NumGoroutine()) fmt.Fprintf(conn, "OS threads: %v\n", pprof.Lookup("threadcreate").Count()) fmt.Fprintf(conn, "GOMAXPROCS: %v\n", runtime.GOMAXPROCS(0)) fmt.Fprintf(conn, "num CPU: %v\n", runtime.NumCPU()) case signal.BinaryDump: path, err := os.Executable() if err != nil { return err } f, err := os.Open(path) if err != nil { return err } defer f.Close() _, err = bufio.NewReader(f).WriteTo(conn) return err case signal.Trace: if err := trace.Start(conn); err != nil { return err } time.Sleep(5 * time.Second) trace.Stop() case signal.SetGCPercent: perc, err := binary.ReadVarint(bufio.NewReader(conn)) if err != nil { return err } fmt.Fprintf(conn, "New GC percent set to %v. Previous value was %v.\n", perc, debug.SetGCPercent(int(perc))) } return nil } ================================================ FILE: agent/agent_test.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. package agent import ( "os" "testing" ) func TestListen(t *testing.T) { err := Listen(Options{}) if err != nil { t.Fatal(err) } Close() } func TestAgentClose(t *testing.T) { err := Listen(Options{}) if err != nil { t.Fatal(err) } Close() _, err = os.Stat(portfile) if !os.IsNotExist(err) { t.Fatalf("portfile = %q doesn't exist; err = %v", portfile, err) } if portfile != "" { t.Fatalf("got = %q; want empty portfile", portfile) } } func TestUseCustomConfigDir(t *testing.T) { err := Listen(Options{ ConfigDir: os.TempDir(), ShutdownCleanup: true, }) if err != nil { t.Fatal(err) } Close() } func TestAgentListenMultipleClose(t *testing.T) { err := Listen(Options{}) if err != nil { t.Fatal(err) } Close() Close() Close() Close() } func TestAgentListenReuseAddrAndPort(t *testing.T) { err := Listen(Options{ Addr: "127.0.0.1:50000", ReuseSocketAddrAndPort: true, }) if err != nil { t.Fatal(err) } Close() } func TestFormatBytes(t *testing.T) { tests := []struct { val uint64 want string }{ {1023, "1023 bytes"}, {1024, "1.00KB (1024 bytes)"}, {1024*1024 - 100, "1023.90KB (1048476 bytes)"}, {1024 * 1024, "1.00MB (1048576 bytes)"}, {1024 * 1025, "1.00MB (1049600 bytes)"}, {1024 * 1024 * 1024, "1.00GB (1073741824 bytes)"}, {1024*1024*1024 + 430*1024*1024, "1.42GB (1524629504 bytes)"}, {1024 * 1024 * 1024 * 1024 * 1024, "1.00PB (1125899906842624 bytes)"}, {1024 * 1024 * 1024 * 1024 * 1024 * 1024, "1024.00PB (1152921504606846976 bytes)"}, } for _, tt := range tests { result := formatBytes(tt.val) if result != tt.want { t.Errorf("formatBytes(%v) = %q; want %q", tt.val, result, tt.want) } } } ================================================ FILE: agent/sockopt_reuseport.go ================================================ // Copyright 2020 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. //go:build !js && !plan9 && !solaris && !windows // +build !js,!plan9,!solaris,!windows package agent import ( "syscall" "golang.org/x/sys/unix" ) // setReuseAddrAndPortSockopts sets the SO_REUSEADDR and SO_REUSEPORT socket // options on c's underlying socket in order to increase the chance to re-bind() // to the same address and port upon agent restart. func setReuseAddrAndPortSockopts(network, address string, c syscall.RawConn) error { var soerr error if err := c.Control(func(su uintptr) { sock := int(su) // Allow reuse of recently-used addresses. This socket option is // set by default on listeners in Go's net package, see // net.setDefaultSockopts. soerr = unix.SetsockoptInt(sock, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) if soerr != nil { return } // Allow reuse of recently-used ports. This gives the agent a // better chance to re-bind upon restarts. soerr = unix.SetsockoptInt(sock, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) }); err != nil { return err } return soerr } ================================================ FILE: agent/sockopt_unsupported.go ================================================ // Copyright 2020 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. //go:build (js && wasm) || plan9 || solaris || windows // +build js,wasm plan9 solaris windows package agent import "syscall" func setReuseAddrAndPortSockopts(network, address string, c syscall.RawConn) error { return nil } ================================================ FILE: cmd.go ================================================ // 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 main ================================================ FILE: examples/hello/main.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. package main import ( "log" "time" "github.com/google/gops/agent" ) func main() { if err := agent.Listen(agent.Options{ ShutdownCleanup: true, // automatically closes on os.Interrupt }); err != nil { log.Fatal(err) } time.Sleep(time.Hour) } ================================================ FILE: go.mod ================================================ module github.com/google/gops go 1.18 require ( github.com/shirou/gopsutil/v3 v3.24.5 github.com/spf13/cobra v1.9.1 github.com/xlab/treeprint v1.2.0 golang.org/x/sys v0.30.0 rsc.io/goversion v1.2.0 ) require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect ) ================================================ FILE: go.sum ================================================ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w= rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= ================================================ FILE: goprocess/goprocess.go ================================================ // 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 goprocess reports the Go processes running on a host. package goprocess import ( "os" "sync" "github.com/google/gops/internal" "github.com/shirou/gopsutil/v3/process" ) // P represents a Go process. type P struct { PID int PPID int Exec string Path string BuildVersion string Agent bool } // FindAll returns all the Go processes currently running on this host. func FindAll() []P { const concurrencyLimit = 10 // max number of concurrent workers pss, err := process.Processes() if err != nil { return nil } return findAll(pss, isGo, concurrencyLimit) } // Allows to inject isGo for testing. type isGoFunc func(*process.Process) (path, version string, agent, ok bool, err error) func findAll(pss []*process.Process, isGo isGoFunc, concurrencyLimit int) []P { input := make(chan *process.Process, len(pss)) output := make(chan P, len(pss)) for _, ps := range pss { input <- ps } close(input) var wg sync.WaitGroup wg.Add(concurrencyLimit) // used to wait for workers to be finished // Run concurrencyLimit of workers until there // is no more processes to be checked in the input channel. for i := 0; i < concurrencyLimit; i++ { go func() { defer wg.Done() for pr := range input { path, version, agent, ok, err := isGo(pr) if err != nil { // TODO(jbd): Return a list of errors. continue } if !ok { continue } ppid, err := pr.Ppid() if err != nil { continue } name, err := pr.Name() if err != nil { continue } output <- P{ PID: int(pr.Pid), PPID: int(ppid), Exec: name, Path: path, BuildVersion: version, Agent: agent, } } }() } wg.Wait() // wait until all workers are finished close(output) // no more results to be waited for var results []P for p := range output { results = append(results, p) } return results } // Find finds info about the process identified with the given PID. func Find(pid int) (P, bool, error) { pr, err := process.NewProcess(int32(pid)) if err != nil { return P{}, false, err } path, version, agent, ok, err := isGo(pr) if !ok || err != nil { return P{}, false, nil } ppid, err := pr.Ppid() if err != nil { return P{}, false, err } name, err := pr.Name() if err != nil { return P{}, false, err } return P{ PID: int(pr.Pid), PPID: int(ppid), Exec: name, Path: path, BuildVersion: version, Agent: agent, }, true, nil } // isGo looks up the runtime.buildVersion symbol // in the process' binary and determines if the process // if a Go process or not. If the process is a Go process, // it reports PID, binary name and full path of the binary. func isGo(pr *process.Process) (path, version string, agent, ok bool, err error) { if pr.Pid == 0 { // ignore system process return } path, err = pr.Exe() if err != nil { return } version, err = goVersion(path) if err != nil { return } ok = true pidfile, err := internal.PIDFile(int(pr.Pid)) if err == nil { _, err := os.Stat(pidfile) agent = err == nil } return path, version, agent, ok, nil } ================================================ FILE: goprocess/goprocess_1.18.go ================================================ // 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. //go:build go1.18 // +build go1.18 package goprocess import "debug/buildinfo" func goVersion(path string) (string, error) { info, err := buildinfo.ReadFile(path) if err != nil { return "", err } return info.GoVersion, nil } ================================================ FILE: goprocess/goprocess_lt1.18.go ================================================ // 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. //go:build !go1.18 // +build !go1.18 package goprocess import goversion "rsc.io/goversion/version" func goVersion(path string) (string, error) { versionInfo, err := goversion.ReadExe(path) if err != nil { return "", err } return versionInfo.Release, nil } ================================================ FILE: goprocess/goprocess_test.go ================================================ package goprocess import ( "os" "path/filepath" "reflect" "runtime" "sort" "strconv" "testing" "github.com/shirou/gopsutil/v3/process" ) func BenchmarkFindAll(b *testing.B) { for ii := 0; ii < b.N; ii++ { _ = FindAll() } } // TestFindAll tests findAll implementation function. func TestFindAll(t *testing.T) { testProcess, err := process.NewProcess(int32(os.Getpid())) if err != nil { t.Errorf("failed to get current process: %v", err) } testPpid, _ := testProcess.Ppid() testExec, _ := testProcess.Name() wantProcess := P{PID: int(testProcess.Pid), PPID: int(testPpid), Exec: testExec} for _, tc := range []struct { name string concurrencyLimit int input []*process.Process goPIDs []int want []P mock bool }{{ name: "no processes", concurrencyLimit: 10, input: nil, want: nil, }, { name: "non-Go process", concurrencyLimit: 10, input: []*process.Process{testProcess}, want: nil, }, { name: "Go process", concurrencyLimit: 10, input: []*process.Process{testProcess}, goPIDs: []int{int(testProcess.Pid)}, want: []P{wantProcess}, }, { name: "filters Go processes", concurrencyLimit: 10, input: fakeProcessesWithPIDs(1, 2, 3, 4, 5, 6, 7), goPIDs: []int{1, 3, 5, 7}, want: []P{{PID: 1}, {PID: 3}, {PID: 5}, {PID: 7}}, mock: true, }, { name: "Go processes above max concurrency (issue #123)", concurrencyLimit: 2, input: fakeProcessesWithPIDs(1, 2, 3, 4, 5, 6, 7), goPIDs: []int{1, 3, 5, 7}, want: []P{{PID: 1}, {PID: 3}, {PID: 5}, {PID: 7}}, mock: true, }} { t.Run(tc.name, func(t *testing.T) { if tc.mock { if runtime.GOOS != "linux" { t.Skip() } tempDir, err := os.MkdirTemp("", "") if err != nil { t.Errorf("failed to create temp dir: %v", err) } defer os.RemoveAll(tempDir) for _, p := range tc.input { os.Mkdir(filepath.Join(tempDir, strconv.Itoa(int(p.Pid))), 0o755) os.WriteFile(filepath.Join(tempDir, strconv.Itoa(int(p.Pid)), "stat"), []byte( `1440024 () R 0 1440024 0 34821 1440024 4194304 134 0 0 0 0 0 0 0 20 0 1 0 95120609 6746112 274 18446744073709551615 94467689938944 94467690036601 140724224197808 0 0 0 0 0 0 0 0 0 17 11 0 0 0 0 0 94467690068048 94467690071296 94467715629056 140724224199226 140724224199259 140724224199259 140724224204780 0`, ), 0o644) os.WriteFile(filepath.Join(tempDir, strconv.Itoa(int(p.Pid)), "status"), []byte( `Name: Umask: 0022 State: R (running) Tgid: 1440366 Ngid: 0 Pid: 1440366 PPid: 0 `, ), 0o644) } os.Setenv("HOST_PROC", tempDir) } actual := findAll(tc.input, fakeIsGo(tc.goPIDs), tc.concurrencyLimit) sort.Slice(actual, func(i, j int) bool { return actual[i].PID < actual[j].PID }) if !reflect.DeepEqual(actual, tc.want) { t.Errorf("findAll(concurrency=%v)\ngot %v\nwant %v", tc.concurrencyLimit, actual, tc.want) } }) } } func fakeIsGo(goPIDs []int) isGoFunc { return func(pr *process.Process) (path, version string, agent, ok bool, err error) { for _, p := range goPIDs { if p == int(pr.Pid) { ok = true return } } return } } func fakeProcessesWithPIDs(pids ...int) []*process.Process { p := make([]*process.Process, 0, len(pids)) for _, pid := range pids { p = append(p, &process.Process{Pid: int32(pid)}) } return p } ================================================ FILE: internal/cmd/process.go ================================================ // 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" "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 [period]", Aliases: []string{"pid", "proc"}, Short: "Prints information about a Go process.", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return ProcessInfo(args) }, } } // ProcessInfo takes arguments starting with pid|:addr and grabs all kinds of // useful Go process information. func ProcessInfo(args []string) error { pid, err := strconv.Atoi(args[0]) if err != nil { return fmt.Errorf("error parsing the first argument: %w", err) } var period time.Duration if len(args) >= 2 { period, err = time.ParseDuration(args[1]) if err != nil { secs, err := strconv.Atoi(args[1]) if err != nil { return fmt.Errorf("error parsing the second argument: %w", err) } period = time.Duration(secs) * time.Second } } processInfo(pid, period) return nil } 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() } ================================================ FILE: internal/cmd/process_test.go ================================================ // 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) } }) } } ================================================ FILE: internal/cmd/root.go ================================================ // 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" "github.com/google/gops/goprocess" "github.com/spf13/cobra" ) // NewRoot command. func NewRoot() *cobra.Command { return &cobra.Command{ Use: "gops", Short: "gops is a tool to list and diagnose Go processes.", Example: ` gops ... gops # displays process info gops help # displays this help message`, // TODO(jbd): add link that explains the use of agent. Run: func(cmd *cobra.Command, args []string) { processes() }, } } 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 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 } ================================================ FILE: internal/cmd/root_test.go ================================================ // 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) } }) } } ================================================ FILE: internal/cmd/shared.go ================================================ // 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" "net" "os" "os/exec" "strconv" "strings" "github.com/google/gops/internal" "github.com/google/gops/signal" "github.com/spf13/cobra" ) // 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. To completely stop GC, set to 'off'", 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 ", 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 { if len(params) != 1 { return errors.New("missing gc percentage") } var ( perc int64 err error ) if strings.ToLower(params[0]) == "off" { perc = -1 } else { 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 := os.CreateTemp("", "trace") if err != nil { return err } if err := os.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 := os.CreateTemp("", 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 := os.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 := os.CreateTemp("", "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 := os.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 := io.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 } ================================================ FILE: internal/cmd/shared_test.go ================================================ // 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) } } } ================================================ FILE: internal/cmd/tree.go ================================================ // 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/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() 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) } } ================================================ FILE: internal/internal.go ================================================ // 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 internal import ( "errors" "os" "os/user" "path/filepath" "strconv" "strings" ) const gopsConfigDirEnvKey = "GOPS_CONFIG_DIR" func ConfigDir() (string, error) { if configDir := os.Getenv(gopsConfigDirEnvKey); configDir != "" { return configDir, nil } if userConfigDir, err := os.UserConfigDir(); err == nil { return filepath.Join(userConfigDir, "gops"), nil } homeDir := guessUnixHomeDir() if homeDir == "" { return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty") } return filepath.Join(homeDir, ".config", "gops"), nil } func guessUnixHomeDir() string { usr, err := user.Current() if err == nil { return usr.HomeDir } return os.Getenv("HOME") } func PIDFile(pid int) (string, error) { gopsdir, err := ConfigDir() if err != nil { return "", err } return filepath.Join(gopsdir, strconv.Itoa(pid)), nil } func GetPort(pid int) (string, error) { portfile, err := PIDFile(pid) if err != nil { return "", err } b, err := os.ReadFile(portfile) if err != nil { return "", err } port := strings.TrimSpace(string(b)) return port, nil } ================================================ FILE: internal/internal_test.go ================================================ package internal import ( "os" "path/filepath" "testing" ) func TestConfigDir(t *testing.T) { configDir, err := ConfigDir() if err != nil { t.Fatal(err) } if g, w := filepath.Base(configDir), "gops"; g != w { t.Errorf("ConfigDir: got base directory %q, want %q", g, w) } key := gopsConfigDirEnvKey oldDir := os.Getenv(key) defer os.Setenv(key, oldDir) newDir := "foo-bar" os.Setenv(key, newDir) configDir, err = ConfigDir() if err != nil { t.Fatal(err) } if g, w := configDir, newDir; g != w { t.Errorf("ConfigDir: got=%v want=%v", g, w) } } ================================================ FILE: main.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 ( "log" "os" "strconv" "github.com/google/gops/internal/cmd" ) func main() { var root = cmd.NewRoot() root.AddCommand(cmd.ProcessCommand()) root.AddCommand(cmd.TreeCommand()) root.AddCommand(cmd.AgentCommands()...) // Legacy support for `gops ` 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 `. 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) } } ================================================ FILE: main_test.go ================================================ // 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 main ================================================ FILE: signal/signal.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. // Package signal contains signals used to communicate to the gops agents. package signal const ( // StackTrace represents a command to print stack trace. StackTrace = byte(0x1) // GC runs the garbage collector. GC = byte(0x2) // MemStats reports memory stats. MemStats = byte(0x3) // Version prints the Go version. Version = byte(0x4) // HeapProfile starts `go tool pprof` with the current memory profile. HeapProfile = byte(0x5) // CPUProfile starts `go tool pprof` with the current CPU profile CPUProfile = byte(0x6) // Stats returns Go runtime statistics such as number of goroutines, GOMAXPROCS, and NumCPU. Stats = byte(0x7) // Trace starts the Go execution tracer, waits 5 seconds and launches the trace tool. Trace = byte(0x8) // BinaryDump returns running binary file. BinaryDump = byte(0x9) // SetGCPercent sets the garbage collection target percentage. SetGCPercent = byte(0x10) )