Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Another take on HTTP mode #41

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 13 additions & 13 deletions README.md
Expand Up @@ -47,9 +47,9 @@ func main() {
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.
To use gops in a remote mode you need to know target's agent address (and HTTP endpoint if using HTTP listener).

In Local mode use process's PID as a target; in Remote mode target is a `host:port` combination.
In Local mode use process's PID as a target; in Remote mode target is a `host:port` combination or HTTP addr (`http://host:port/handler`).

#### 0. Listing all processes running locally

Expand All @@ -65,13 +65,13 @@ $ gops

Note that processes running the agent are marked with `*` next to the PID (e.g. `4132*`).

#### $ gops stack (\<pid\>|\<addr\>)
#### $ gops stack (\<pid\>|\<addr\>|\<endpoint\>)

In order to print the current stack trace from a target program, run the following command:


```sh
$ gops stack (<pid>|<addr>)
$ gops stack (<pid>|<addr>|<endpoint>)
gops stack 85709
goroutine 8 [running]:
runtime/pprof.writeGoroutineStacks(0x13c7bc0, 0xc42000e008, 0xc420ec8520, 0xc420ec8520)
Expand All @@ -89,31 +89,31 @@ created by github.com/google/gops/agent.Listen
# ...
```

#### $ gops memstats (\<pid\>|\<addr\>)
#### $ gops memstats (\<pid\>|\<addr\>|\<endpoint\>)

To print the current memory stats, run the following command:

```sh
$ gops memstats (<pid>|<addr>)
$ gops memstats (<pid>|<addr>|<endpoint>)
```


#### $ gops gc (\<pid\>|\<addr\>)
#### $ gops gc (\<pid\>|\<addr\>|\<endpoint\>)

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\>|\<addr\>)
#### $ gops version (\<pid\>|\<addr\>|\<endpoint\>)

gops reports the Go version the target program is built with, if you run the following:

```sh
$ gops version (<pid>|<addr>)
$ gops version (<pid>|<addr>|<endpoint>)
devel +6a3c6c0 Sat Jan 14 05:57:07 2017 +0000
```

#### $ gops stats (\<pid\>|\<addr\>)
#### $ gops stats (\<pid\>|\<addr\>|\<endpoint\>)

To print the runtime statistics such as number of goroutines and `GOMAXPROCS`.

Expand All @@ -128,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>|<addr>)
$ gops pprof-cpu (<pid>|<addr>|<endpoint>)
```

To enter the heap profile, run:

```sh
$ gops pprof-heap (<pid>|<addr>)
$ gops pprof-heap (<pid>|<addr>|<endpoint>)
```

##### Execution trace

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

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

6 changes: 3 additions & 3 deletions agent/agent.go
Expand Up @@ -116,7 +116,7 @@ func listen() {
fmt.Fprintf(os.Stderr, "gops: %v", err)
continue
}
if err := handle(fd, buf); err != nil {
if err := handle(fd, buf[0]); err != nil {
fmt.Fprintf(os.Stderr, "gops: %v", err)
continue
}
Expand Down Expand Up @@ -165,8 +165,8 @@ func formatBytes(val uint64) string {
return fmt.Sprintf("%d bytes", val)
}

func handle(conn io.Writer, msg []byte) error {
switch msg[0] {
func handle(conn io.Writer, msg byte) error {
switch msg {
case signal.StackTrace:
return pprof.Lookup("goroutine").WriteTo(conn, 2)
case signal.GC:
Expand Down
25 changes: 25 additions & 0 deletions agent/http.go
@@ -0,0 +1,25 @@
package agent

import (
"net/http"

"github.com/google/gops/signal"
)

// HandlerFunc returns a function that handles gops requests over HTTP.
func HandlerFunc() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sig, ok := signal.FromParam(r.URL.Query().Get("action"))
if !ok {
w.WriteHeader(400)
_, _ = w.Write([]byte("Unknown action!"))
return
}
w.Header().Set("Content-Type", "application/octet-stream")
err := handle(w, sig)
if err != nil {
w.WriteHeader(500)
_, _ = w.Write([]byte(err.Error()))
}
}
}
90 changes: 90 additions & 0 deletions client.go
@@ -0,0 +1,90 @@
package main

import (
"fmt"
"io"
"io/ioutil"
"net"
"net/http"

"net/url"

"github.com/google/gops/signal"
"github.com/pkg/errors"
)

type Client interface {
Run(byte) ([]byte, error)
RunReader(byte) (io.ReadCloser, error)
}

type ClientTCP struct {
addr net.TCPAddr
}

func (c *ClientTCP) Run(sig byte) ([]byte, error) {
return c.run(sig)
}

func (c *ClientTCP) RunReader(sig byte) (io.ReadCloser, error) {
return c.runLazy(sig)
}

func (c *ClientTCP) runLazy(sig byte) (io.ReadCloser, error) {
conn, err := net.DialTCP("tcp", nil, &c.addr)
if err != nil {
return nil, err
}
if _, err := conn.Write([]byte{sig}); err != nil {
return nil, err
}
return conn, nil
}

func (c *ClientTCP) run(sig byte) ([]byte, error) {
r, err := c.runLazy(sig)
defer r.Close()
if err != nil {
return nil, err
}
return ioutil.ReadAll(r)
}

type ClientHTTP struct {
baseAddr string
}

func (c *ClientHTTP) Run(sig byte) ([]byte, error) {
r, err := c.RunReader(sig)
if err != nil {
return nil, err
}
defer r.Close()
if err != nil {
return nil, err
}
return ioutil.ReadAll(r)
}

func (c *ClientHTTP) RunReader(sig byte) (io.ReadCloser, error) {
action, ok := signal.ToParam(sig)
if !ok {
return nil, fmt.Errorf("unknown signal %v", sig)
}
client := &http.Client{}

values := url.Values{}
values.Set("action", action)

req, _ := http.NewRequest("GET", c.baseAddr, nil)
req.URL.RawQuery = values.Encode()

rsp, err := client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "error when making HTTP call")
}
if rsp.StatusCode != http.StatusOK {
return nil, errors.Errorf("Server returned HTTP %v", rsp.StatusCode)
}
return rsp.Body, nil
}
79 changes: 30 additions & 49 deletions cmd.go
Expand Up @@ -3,7 +3,6 @@ package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
Expand All @@ -15,7 +14,7 @@ import (
"github.com/google/gops/signal"
)

var cmds = map[string](func(addr net.TCPAddr) error){
var cmds = map[string](func(cli Client) error){
"stack": stackTrace,
"gc": gc,
"memstats": memStats,
Expand All @@ -26,35 +25,35 @@ var cmds = map[string](func(addr net.TCPAddr) error){
"trace": trace,
}

func stackTrace(addr net.TCPAddr) error {
return cmdWithPrint(addr, signal.StackTrace)
func stackTrace(cli Client) error {
return cmdWithPrint(cli, signal.StackTrace)
}

func gc(addr net.TCPAddr) error {
_, err := cmd(addr, signal.GC)
func gc(cli Client) error {
_, err := cli.Run(signal.GC)
return err
}

func memStats(addr net.TCPAddr) error {
return cmdWithPrint(addr, signal.MemStats)
func memStats(cli Client) error {
return cmdWithPrint(cli, signal.MemStats)
}

func version(addr net.TCPAddr) error {
return cmdWithPrint(addr, signal.Version)
func version(cli Client) error {
return cmdWithPrint(cli, signal.Version)
}

func pprofHeap(addr net.TCPAddr) error {
return pprof(addr, signal.HeapProfile)
func pprofHeap(cli Client) error {
return pprof(cli, signal.HeapProfile)
}

func pprofCPU(addr net.TCPAddr) error {
func pprofCPU(cli Client) error {
fmt.Println("Profiling CPU now, will take 30 secs...")
return pprof(addr, signal.CPUProfile)
return pprof(cli, signal.CPUProfile)
}

func trace(addr net.TCPAddr) error {
func trace(cli Client) error {
fmt.Println("Tracing now, will take 5 secs...")
out, err := cmd(addr, signal.Trace)
out, err := cli.Run(signal.Trace)
if err != nil {
return err
}
Expand All @@ -77,14 +76,14 @@ func trace(addr net.TCPAddr) error {
return cmd.Run()
}

func pprof(addr net.TCPAddr, p byte) error {
func pprof(cli Client, p byte) error {

tmpDumpFile, err := ioutil.TempFile("", "profile")
if err != nil {
return err
}
{
out, err := cmd(addr, p)
out, err := cli.Run(p)
if err != nil {
return err
}
Expand All @@ -102,7 +101,7 @@ func pprof(addr net.TCPAddr, p byte) error {
return err
}
{
out, err := cmd(addr, signal.BinaryDump)
out, err := cli.Run(signal.BinaryDump)
if err != nil {
return fmt.Errorf("failed to read the binary: %v", err)
}
Expand All @@ -122,61 +121,43 @@ func pprof(addr net.TCPAddr, p byte) error {
return cmd.Run()
}

func stats(addr net.TCPAddr) error {
return cmdWithPrint(addr, signal.Stats)
func stats(cli Client) error {
return cmdWithPrint(cli, signal.Stats)
}

func cmdWithPrint(addr net.TCPAddr, c byte) error {
out, err := cmd(addr, c)
func cmdWithPrint(cli Client, c byte) error {
out, err := cli.Run(c)
if err != nil {
return err
}
fmt.Printf("%s", out)
return nil
}

// targetToAddr tries to parse the target string, be it remote host:port
// targetToClient tries to parse the target string, be it remote host:port
// or local process's PID.
func targetToAddr(target string) (*net.TCPAddr, error) {
func targetToClient(target string) (Client, error) {
if strings.HasPrefix(target, "http:") || strings.HasPrefix(target, "https:") {
return &ClientHTTP{baseAddr: target}, nil
}
if strings.Index(target, ":") != -1 {
// 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
return &ClientTCP{addr: *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)
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, 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) (io.Reader, error) {
conn, err := net.DialTCP("tcp", nil, &addr)
if err != nil {
return nil, err
}
if _, err := conn.Write([]byte{c}); err != nil {
return nil, err
}
return conn, nil
addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:"+port)
return &ClientTCP{addr: *addr}, nil
}