1
0
mirror of https://github.com/google/gops.git synced 2025-04-11 11:12:01 +02:00

add remote mode support (#30)

This commit is contained in:
Nick K 2017-03-02 23:51:14 +04:00 committed by Jaana B. Dogan
parent c8ccacaea5
commit 288e543fee
6 changed files with 130 additions and 71 deletions

View File

@ -44,7 +44,14 @@ func main() {
### Manual
#### Listing all processes
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.
#### 0. Listing all processes running locally
To print all go processes, run `gops` without arguments:
@ -58,13 +65,13 @@ $ gops
Note that processes running the agent are marked with `*` next to the PID (e.g. `4132*`).
#### $ gops stack \<pid\>
#### $ gops stack (\<pid\>|\<addr\>)
In order to print the current stack trace from a target program, run the following command:
```sh
$ gops stack <pid>
$ gops stack (<pid>|<addr>)
gops stack 85709
goroutine 8 [running]:
runtime/pprof.writeGoroutineStacks(0x13c7bc0, 0xc42000e008, 0xc420ec8520, 0xc420ec8520)
@ -82,31 +89,31 @@ created by github.com/google/gops/agent.Listen
# ...
```
#### $ gops memstats \<pid\>
#### $ gops memstats (\<pid\>|\<addr\>)
To print the current memory stats, run the following command:
```sh
$ gops memstats <pid>
$ gops memstats (<pid>|<addr>)
```
#### $ gops gc \<pid\>
#### $ gops gc (\<pid\>|\<addr\>)
If you want to force run garbage collection on the target program, run `gc`.
It will block until the GC is completed.
#### $ gops version \<pid\>
#### $ gops version (\<pid\>|\<addr\>)
gops reports the Go version the target program is built with, if you run the following:
```sh
$ gops version <pid>
$ gops version (<pid>|<addr>)
devel +6a3c6c0 Sat Jan 14 05:57:07 2017 +0000
```
#### $ gops stats \<pid\>
#### $ gops stats (\<pid\>|\<addr\>)
To print the runtime statistics such as number of goroutines and `GOMAXPROCS`.
@ -121,13 +128,13 @@ it shells out to the `go tool pprof` and let you interatively examine the profil
To enter the CPU profile, run:
```sh
$ gops pprof-cpu <pid>
$ gops pprof-cpu (<pid>|<addr>)
```
To enter the heap profile, run:
```sh
$ gops pprof-heap <pid>
$ gops pprof-heap (<pid>|<addr>)
```
##### Go execution trace
@ -135,6 +142,6 @@ $ gops pprof-heap <pid>
gops allows you to start the runtime tracer for 5 seconds and examine the results.
```sh
$ gops trace <pid>
$ gops trace (<pid>|<addr>)
```

View File

@ -20,8 +20,11 @@ import (
"sync"
"time"
"bufio"
"github.com/google/gops/internal"
"github.com/google/gops/signal"
"github.com/kardianos/osext"
)
const defaultAddr = "127.0.0.1:0"
@ -212,6 +215,19 @@ func handle(conn io.Writer, msg []byte) error {
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 := osext.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:
trace.Start(conn)
time.Sleep(5 * time.Second)

131
cmd.go
View File

@ -1,20 +1,21 @@
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"os/exec"
"strconv"
"strings"
"github.com/google/gops/internal"
"github.com/google/gops/signal"
ps "github.com/keybase/go-ps"
"github.com/pkg/errors"
)
var cmds = map[string](func(pid int) error){
var cmds = map[string](func(addr net.TCPAddr) error){
"stack": stackTrace,
"gc": gc,
"memstats": memStats,
@ -25,35 +26,35 @@ var cmds = map[string](func(pid int) error){
"trace": trace,
}
func stackTrace(pid int) error {
return cmdWithPrint(pid, signal.StackTrace)
func stackTrace(addr net.TCPAddr) error {
return cmdWithPrint(addr, signal.StackTrace)
}
func gc(pid int) error {
_, err := cmd(pid, signal.GC)
func gc(addr net.TCPAddr) error {
_, err := cmd(addr, signal.GC)
return err
}
func memStats(pid int) error {
return cmdWithPrint(pid, signal.MemStats)
func memStats(addr net.TCPAddr) error {
return cmdWithPrint(addr, signal.MemStats)
}
func version(pid int) error {
return cmdWithPrint(pid, signal.Version)
func version(addr net.TCPAddr) error {
return cmdWithPrint(addr, signal.Version)
}
func pprofHeap(pid int) error {
return pprof(pid, signal.HeapProfile)
func pprofHeap(addr net.TCPAddr) error {
return pprof(addr, signal.HeapProfile)
}
func pprofCPU(pid int) error {
func pprofCPU(addr net.TCPAddr) error {
fmt.Println("Profiling CPU now, will take 30 secs...")
return pprof(pid, signal.CPUProfile)
return pprof(addr, signal.CPUProfile)
}
func trace(pid int) error {
func trace(addr net.TCPAddr) error {
fmt.Println("Tracing now, will take 5 secs...")
out, err := cmd(pid, signal.Trace)
out, err := cmd(addr, signal.Trace)
if err != nil {
return err
}
@ -76,32 +77,45 @@ func trace(pid int) error {
return cmd.Run()
}
func pprof(pid int, p byte) error {
out, err := cmd(pid, p)
func pprof(addr net.TCPAddr, p byte) error {
tmpDumpFile, err := ioutil.TempFile("", "profile")
if err != nil {
return err
}
if len(out) == 0 {
return errors.New("failed to read the profile")
{
out, err := cmd(addr, p)
if err != nil {
return err
}
if len(out) == 0 {
return errors.New("failed to read the profile")
}
defer os.Remove(tmpDumpFile.Name())
if err := ioutil.WriteFile(tmpDumpFile.Name(), out, 0); err != nil {
return err
}
}
tmpfile, err := ioutil.TempFile("", "profile")
// Download running binary
tmpBinFile, err := ioutil.TempFile("", "binary")
if err != nil {
return err
}
defer os.Remove(tmpfile.Name())
if err := ioutil.WriteFile(tmpfile.Name(), out, 0); err != nil {
return err
{
out, err := cmd(addr, signal.BinaryDump)
if err != nil {
return errors.New("couldn't retrieve running binary's dump")
}
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
}
}
process, err := ps.FindProcess(pid)
if err != nil {
// TODO(jbd): add context to the error
return err
}
binary, err := process.Path()
if err != nil {
return fmt.Errorf("cannot the binary for the PID: %v", err)
}
cmd := exec.Command("go", "tool", "pprof", binary, tmpfile.Name())
cmd := exec.Command("go", "tool", "pprof", tmpBinFile.Name(), tmpDumpFile.Name())
cmd.Env = os.Environ()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
@ -109,12 +123,12 @@ func pprof(pid int, p byte) error {
return cmd.Run()
}
func stats(pid int) error {
return cmdWithPrint(pid, signal.Stats)
func stats(addr net.TCPAddr) error {
return cmdWithPrint(addr, signal.Stats)
}
func cmdWithPrint(pid int, c byte) error {
out, err := cmd(pid, c)
func cmdWithPrint(addr net.TCPAddr, c byte) error {
out, err := cmd(addr, c)
if err != nil {
return err
}
@ -122,11 +136,34 @@ func cmdWithPrint(pid int, c byte) error {
return nil
}
func cmd(pid int, c byte) ([]byte, error) {
conn, err := cmdLazy(pid, c)
if err != nil {
return nil, err
// 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.Index(target, ":") != -1 {
// addr host:port passed
var err error
addr, err := net.ResolveTCPAddr("tcp", target)
if err != nil {
return nil, errors.Wrap(err, "couldn't parse dst address")
}
return addr, nil
}
// try to find port by pid then, connect to local
pid, err := strconv.Atoi(target)
if err != nil {
return nil, errors.Wrap(err, "couldn't parse PID")
}
port, err := internal.GetPort(pid)
addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:"+port)
return addr, nil
}
func cmd(addr net.TCPAddr, c byte) ([]byte, error) {
conn, err := cmdLazy(addr, c)
if err != nil {
return nil, errors.Wrap(err, "couldn't get port by PID")
}
all, err := ioutil.ReadAll(conn)
if err != nil {
return nil, err
@ -134,12 +171,8 @@ func cmd(pid int, c byte) ([]byte, error) {
return all, nil
}
func cmdLazy(pid int, c byte) (io.Reader, error) {
port, err := internal.GetPort(pid)
if err != nil {
return nil, err
}
conn, err := net.Dial("tcp", "127.0.0.1:"+port)
func cmdLazy(addr net.TCPAddr, c byte) (io.Reader, error) {
conn, err := net.DialTCP("tcp", nil, &addr)
if err != nil {
return nil, err
}

View File

@ -12,7 +12,9 @@ import (
)
func main() {
if err := agent.Listen(nil); err != nil {
if err := agent.Listen(&agent.Options{
Addr: "127.0.0.1:4321",
}); err != nil {
log.Fatal(err)
}
time.Sleep(time.Hour)

16
main.go
View File

@ -10,7 +10,6 @@ import (
"fmt"
"log"
"os"
"strconv"
"sync"
"github.com/google/gops/internal"
@ -20,8 +19,6 @@ import (
const helpText = `Usage: gops is a tool to list and diagnose Go processes.
gops Lists all Go processes currently running.
gops cmd <pid> See the commands below.
Commands:
stack Prints the stack trace.
@ -53,17 +50,18 @@ func main() {
usage("")
}
if len(os.Args) < 3 {
usage("missing PID")
}
pid, err := strconv.Atoi(os.Args[2])
if err != nil {
usage("PID should be numeric")
usage("missing PID or address")
}
fn, ok := cmds[cmd]
if !ok {
usage("unknown subcommand")
}
if err := fn(pid); err != nil {
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)
}
if err := fn(*addr); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}

View File

@ -29,4 +29,7 @@ const (
// 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)
)