1
0
mirror of https://github.com/google/gops.git synced 2025-04-17 11:26:22 +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 ### 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: 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*`). 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: In order to print the current stack trace from a target program, run the following command:
```sh ```sh
$ gops stack <pid> $ gops stack (<pid>|<addr>)
gops stack 85709 gops stack 85709
goroutine 8 [running]: goroutine 8 [running]:
runtime/pprof.writeGoroutineStacks(0x13c7bc0, 0xc42000e008, 0xc420ec8520, 0xc420ec8520) 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: To print the current memory stats, run the following command:
```sh ```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`. If you want to force run garbage collection on the target program, run `gc`.
It will block until the GC is completed. 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: gops reports the Go version the target program is built with, if you run the following:
```sh ```sh
$ gops version <pid> $ gops version (<pid>|<addr>)
devel +6a3c6c0 Sat Jan 14 05:57:07 2017 +0000 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`. 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: To enter the CPU profile, run:
```sh ```sh
$ gops pprof-cpu <pid> $ gops pprof-cpu (<pid>|<addr>)
``` ```
To enter the heap profile, run: To enter the heap profile, run:
```sh ```sh
$ gops pprof-heap <pid> $ gops pprof-heap (<pid>|<addr>)
``` ```
##### Go execution trace ##### 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. gops allows you to start the runtime tracer for 5 seconds and examine the results.
```sh ```sh
$ gops trace <pid> $ gops trace (<pid>|<addr>)
``` ```

View File

@ -20,8 +20,11 @@ import (
"sync" "sync"
"time" "time"
"bufio"
"github.com/google/gops/internal" "github.com/google/gops/internal"
"github.com/google/gops/signal" "github.com/google/gops/signal"
"github.com/kardianos/osext"
) )
const defaultAddr = "127.0.0.1:0" 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, "OS threads: %v\n", pprof.Lookup("threadcreate").Count())
fmt.Fprintf(conn, "GOMAXPROCS: %v\n", runtime.GOMAXPROCS(0)) fmt.Fprintf(conn, "GOMAXPROCS: %v\n", runtime.GOMAXPROCS(0))
fmt.Fprintf(conn, "num CPU: %v\n", runtime.NumCPU()) 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: case signal.Trace:
trace.Start(conn) trace.Start(conn)
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)

131
cmd.go
View File

@ -1,20 +1,21 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net" "net"
"os" "os"
"os/exec" "os/exec"
"strconv"
"strings"
"github.com/google/gops/internal" "github.com/google/gops/internal"
"github.com/google/gops/signal" "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, "stack": stackTrace,
"gc": gc, "gc": gc,
"memstats": memStats, "memstats": memStats,
@ -25,35 +26,35 @@ var cmds = map[string](func(pid int) error){
"trace": trace, "trace": trace,
} }
func stackTrace(pid int) error { func stackTrace(addr net.TCPAddr) error {
return cmdWithPrint(pid, signal.StackTrace) return cmdWithPrint(addr, signal.StackTrace)
} }
func gc(pid int) error { func gc(addr net.TCPAddr) error {
_, err := cmd(pid, signal.GC) _, err := cmd(addr, signal.GC)
return err return err
} }
func memStats(pid int) error { func memStats(addr net.TCPAddr) error {
return cmdWithPrint(pid, signal.MemStats) return cmdWithPrint(addr, signal.MemStats)
} }
func version(pid int) error { func version(addr net.TCPAddr) error {
return cmdWithPrint(pid, signal.Version) return cmdWithPrint(addr, signal.Version)
} }
func pprofHeap(pid int) error { func pprofHeap(addr net.TCPAddr) error {
return pprof(pid, signal.HeapProfile) 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...") 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...") fmt.Println("Tracing now, will take 5 secs...")
out, err := cmd(pid, signal.Trace) out, err := cmd(addr, signal.Trace)
if err != nil { if err != nil {
return err return err
} }
@ -76,32 +77,45 @@ func trace(pid int) error {
return cmd.Run() return cmd.Run()
} }
func pprof(pid int, p byte) error { func pprof(addr net.TCPAddr, p byte) error {
out, err := cmd(pid, p)
tmpDumpFile, err := ioutil.TempFile("", "profile")
if err != nil { if err != nil {
return err 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 { if err != nil {
return err 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) cmd := exec.Command("go", "tool", "pprof", tmpBinFile.Name(), tmpDumpFile.Name())
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.Env = os.Environ() cmd.Env = os.Environ()
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@ -109,12 +123,12 @@ func pprof(pid int, p byte) error {
return cmd.Run() return cmd.Run()
} }
func stats(pid int) error { func stats(addr net.TCPAddr) error {
return cmdWithPrint(pid, signal.Stats) return cmdWithPrint(addr, signal.Stats)
} }
func cmdWithPrint(pid int, c byte) error { func cmdWithPrint(addr net.TCPAddr, c byte) error {
out, err := cmd(pid, c) out, err := cmd(addr, c)
if err != nil { if err != nil {
return err return err
} }
@ -122,11 +136,34 @@ func cmdWithPrint(pid int, c byte) error {
return nil return nil
} }
func cmd(pid int, c byte) ([]byte, error) { // targetToAddr tries to parse the target string, be it remote host:port
conn, err := cmdLazy(pid, c) // or local process's PID.
if err != nil { func targetToAddr(target string) (*net.TCPAddr, error) {
return nil, err 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) all, err := ioutil.ReadAll(conn)
if err != nil { if err != nil {
return nil, err return nil, err
@ -134,12 +171,8 @@ func cmd(pid int, c byte) ([]byte, error) {
return all, nil return all, nil
} }
func cmdLazy(pid int, c byte) (io.Reader, error) { func cmdLazy(addr net.TCPAddr, c byte) (io.Reader, error) {
port, err := internal.GetPort(pid) conn, err := net.DialTCP("tcp", nil, &addr)
if err != nil {
return nil, err
}
conn, err := net.Dial("tcp", "127.0.0.1:"+port)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -12,7 +12,9 @@ import (
) )
func main() { 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) log.Fatal(err)
} }
time.Sleep(time.Hour) time.Sleep(time.Hour)

16
main.go
View File

@ -10,7 +10,6 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"strconv"
"sync" "sync"
"github.com/google/gops/internal" "github.com/google/gops/internal"
@ -20,8 +19,6 @@ import (
const helpText = `Usage: gops is a tool to list and diagnose Go processes. 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: Commands:
stack Prints the stack trace. stack Prints the stack trace.
@ -53,17 +50,18 @@ func main() {
usage("") usage("")
} }
if len(os.Args) < 3 { if len(os.Args) < 3 {
usage("missing PID") usage("missing PID or address")
}
pid, err := strconv.Atoi(os.Args[2])
if err != nil {
usage("PID should be numeric")
} }
fn, ok := cmds[cmd] fn, ok := cmds[cmd]
if !ok { if !ok {
usage("unknown subcommand") 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) fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1) 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 starts the Go execution tracer, waits 5 seconds and launches the trace tool.
Trace = byte(0x8) Trace = byte(0x8)
// BinaryDump returns running binary file.
BinaryDump = byte(0x9)
) )