Skip to content

Commit

Permalink
add remote mode support (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
utrack authored and Jaana B. Dogan committed Mar 2, 2017
1 parent c8ccaca commit 288e543
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 72 deletions.
31 changes: 19 additions & 12 deletions README.md
Expand Up @@ -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:

Expand All @@ -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)
Expand All @@ -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`.

Expand All @@ -121,20 +128,20 @@ 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

gops allows you to start the runtime tracer for 5 seconds and examine the results.

```sh
$ gops trace <pid>
$ gops trace (<pid>|<addr>)
```

16 changes: 16 additions & 0 deletions agent/agent.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
133 changes: 83 additions & 50 deletions cmd.go
@@ -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,
Expand All @@ -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
}
Expand All @@ -76,70 +77,102 @@ func trace(pid int) error {
return cmd.Run()
}

func pprof(pid int, p byte) error {
out, err := cmd(pid, p)
if err != nil {
return err
}
if len(out) == 0 {
return errors.New("failed to read the profile")
}
tmpfile, err := ioutil.TempFile("", "profile")
func pprof(addr net.TCPAddr, p byte) error {

tmpDumpFile, err := ioutil.TempFile("", "profile")
if err != nil {
return err
}
defer os.Remove(tmpfile.Name())
if err := ioutil.WriteFile(tmpfile.Name(), out, 0); err != nil {
return err
}
process, err := ps.FindProcess(pid)
{
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
}
}
// Download running binary
tmpBinFile, err := ioutil.TempFile("", "binary")
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())
{

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
}
}
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(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
}
fmt.Printf("%s", out)
return nil
}

func cmd(pid int, c byte) ([]byte, error) {
conn, err := cmdLazy(pid, c)
// 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, err
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
}
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
}
Expand Down
4 changes: 3 additions & 1 deletion examples/hello/main.go
Expand Up @@ -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)
Expand Down
16 changes: 7 additions & 9 deletions main.go
Expand Up @@ -10,7 +10,6 @@ import (
"fmt"
"log"
"os"
"strconv"
"sync"

"github.com/google/gops/internal"
Expand All @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions signal/signal.go
Expand Up @@ -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)
)

0 comments on commit 288e543

Please sign in to comment.