diff --git a/README.md b/README.md index adbbd700..d8262852 100644 --- a/README.md +++ b/README.md @@ -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 @@ -65,13 +65,13 @@ $ gops Note that processes running the agent are marked with `*` next to the PID (e.g. `4132*`). -#### $ gops stack (\|\) +#### $ gops stack (\|\|\) In order to print the current stack trace from a target program, run the following command: ```sh -$ gops stack (|) +$ gops stack (||) gops stack 85709 goroutine 8 [running]: runtime/pprof.writeGoroutineStacks(0x13c7bc0, 0xc42000e008, 0xc420ec8520, 0xc420ec8520) @@ -89,31 +89,31 @@ created by github.com/google/gops/agent.Listen # ... ``` -#### $ gops memstats (\|\) +#### $ gops memstats (\|\|\) To print the current memory stats, run the following command: ```sh -$ gops memstats (|) +$ gops memstats (||) ``` -#### $ gops gc (\|\) +#### $ 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 version (\|\) +#### $ gops version (\|\|\) gops reports the Go version the target program is built with, if you run the following: ```sh -$ gops version (|) +$ gops version (||) devel +6a3c6c0 Sat Jan 14 05:57:07 2017 +0000 ``` -#### $ gops stats (\|\) +#### $ gops stats (\|\|\) To print the runtime statistics such as number of goroutines and `GOMAXPROCS`. @@ -128,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 (|) +$ gops pprof-cpu (||) ``` To enter the heap profile, run: ```sh -$ gops pprof-heap (|) +$ gops pprof-heap (||) ``` ##### Execution trace @@ -142,6 +142,6 @@ $ gops pprof-heap (|) gops allows you to start the runtime tracer for 5 seconds and examine the results. ```sh -$ gops trace (|) +$ gops trace (||) ``` diff --git a/agent/agent.go b/agent/agent.go index 57083918..6389f13b 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -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 } @@ -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: diff --git a/agent/http.go b/agent/http.go new file mode 100644 index 00000000..4bbe7306 --- /dev/null +++ b/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())) + } + } +} diff --git a/client.go b/client.go new file mode 100644 index 00000000..d4847bae --- /dev/null +++ b/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 +} diff --git a/cmd.go b/cmd.go index f7108ce8..3d9f09f5 100644 --- a/cmd.go +++ b/cmd.go @@ -3,7 +3,6 @@ package main import ( "errors" "fmt" - "io" "io/ioutil" "net" "os" @@ -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, @@ -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 } @@ -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 } @@ -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) } @@ -122,12 +121,12 @@ 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 } @@ -135,9 +134,12 @@ func cmdWithPrint(addr net.TCPAddr, c byte) error { 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 @@ -145,7 +147,7 @@ func targetToAddr(target string) (*net.TCPAddr, error) { 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) @@ -153,30 +155,9 @@ func targetToAddr(target string) (*net.TCPAddr, error) { 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 } diff --git a/examples/hello/main.go b/examples/hello/main.go index 2ee3479a..89395f19 100644 --- a/examples/hello/main.go +++ b/examples/hello/main.go @@ -5,15 +5,44 @@ package main import ( + "fmt" "log" "time" + "net/http" + + "net" + + "math" + + "io/ioutil" + "github.com/google/gops/agent" ) func main() { + // Do work in parallel + go doWork() + + // Serve HTTP handler to be available in HTTP mode + l, err := net.Listen("tcp", "127.0.0.1:12345") + if err != nil { + log.Fatal(err) + } + go http.Serve(l, agent.HandlerFunc()) + + // Use raw socket to accept connections if err := agent.Listen(nil); err != nil { log.Fatal(err) } time.Sleep(time.Hour) } + +func doWork() { + // Emulate some work for non-empty profile) + for i := 0; ; i++ { + res := math.Log(float64(i)) + ioutil.Discard.Write([]byte(fmt.Sprint(res))) + <-time.After(time.Millisecond * 50) + } +} diff --git a/main.go b/main.go index da52a4c2..1849a54c 100644 --- a/main.go +++ b/main.go @@ -56,12 +56,12 @@ func main() { if !ok { usage("unknown subcommand") } - addr, err := targetToAddr(os.Args[2]) + cli, err := targetToClient(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 { + if err := fn(cli); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } diff --git a/signal/signal.go b/signal/signal.go index b2bfbe15..670a00cc 100644 --- a/signal/signal.go +++ b/signal/signal.go @@ -33,3 +33,40 @@ const ( // BinaryDump returns running binary file. BinaryDump = byte(0x9) ) + +// httpPathToSignal maps HTTP request param values to signals. +var httpPathToSignal = map[string]byte{ + "stacktrace": StackTrace, + "gc": GC, + "memstats": MemStats, + "version": Version, + "prof-heap": HeapProfile, + "prof-cpu": CPUProfile, + "stats": Stats, + "trace": Trace, + "binary": BinaryDump, +} + +// toHTTPPath maps signals to HTTP request params. +var toHTTPPath = map[byte]string{} + +func init() { + // Fill second lookup map from first + for k, v := range httpPathToSignal { + toHTTPPath[v] = k + } +} + +// ToParam returns HTTP param associated with given signal. +// Boolean is false if signal was not found. +func ToParam(sig byte) (string, bool) { + ret, ok := toHTTPPath[sig] + return ret, ok +} + +// FromParam returns signal associated with given HTTP parameter. +// Boolean is false if signal was not found. +func FromParam(param string) (byte, bool) { + ret, ok := httpPathToSignal[param] + return ret, ok +}