veyron/tools/debug: Add debug command-line tool

This change adds a command-line tool to interact with the debug server
that's built into all IPC servers. We can access log files, inspect
stats, and run pprof.

$ export NAMESPACE_ROOT=/proxy.envyor.com:8101

$ ./debug glob tunnel/hostname/rpi001/__debug/*
tunnel/hostname/rpi001/__debug/logs
tunnel/hostname/rpi001/__debug/pprof
tunnel/hostname/rpi001/__debug/stats

$ ./debug glob tunnel/hostname/rpi001/__debug/logs/*INFO
tunnel/hostname/rpi001/__debug/logs/tunneld.INFO

$ ./debug logs read tunnel/hostname/rpi001/__debug/logs/tunneld.INFO
[...]

$ ./debug pprof run tunnel/hostname/rpi001/__debug/pprof heap -text
[...]

$ ./debug stats watchglob tunnel/hostname/rpi001/__debug/stats/...
[...]

etc.

Change-Id: I36dc6d8b77b97a76548589b1d215e952f5541714
diff --git a/tools/debug/impl/impl.go b/tools/debug/impl/impl.go
new file mode 100644
index 0000000..7499c55
--- /dev/null
+++ b/tools/debug/impl/impl.go
@@ -0,0 +1,513 @@
+package impl
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"regexp"
+	"sort"
+	"strings"
+
+	"veyron.io/veyron/veyron/lib/cmdline"
+	"veyron.io/veyron/veyron/lib/glob"
+	"veyron.io/veyron/veyron/lib/signals"
+	"veyron.io/veyron/veyron/services/mgmt/pprof/client"
+	istats "veyron.io/veyron/veyron/services/mgmt/stats"
+
+	"veyron.io/veyron/veyron2/context"
+	"veyron.io/veyron/veyron2/naming"
+	"veyron.io/veyron/veyron2/rt"
+	"veyron.io/veyron/veyron2/services/mgmt/logreader"
+	logtypes "veyron.io/veyron/veyron2/services/mgmt/logreader/types"
+	"veyron.io/veyron/veyron2/services/mgmt/pprof"
+	"veyron.io/veyron/veyron2/services/mgmt/stats"
+	"veyron.io/veyron/veyron2/services/mounttable"
+	mttypes "veyron.io/veyron/veyron2/services/mounttable/types"
+	"veyron.io/veyron/veyron2/services/watch"
+	watchtypes "veyron.io/veyron/veyron2/services/watch/types"
+	"veyron.io/veyron/veyron2/vom"
+)
+
+var (
+	follow     bool
+	verbose    bool
+	numEntries int
+	startPos   int64
+	raw        bool
+	showType   bool
+	pprofCmd   string
+)
+
+func init() {
+	vom.Register(istats.HistogramValue{})
+
+	// logs read flags
+	cmdRead.Flags.BoolVar(&follow, "f", false, "When true, read will wait for new log entries when it reaches the end of the file.")
+	cmdRead.Flags.BoolVar(&verbose, "v", false, "When true, read will be more verbose.")
+	cmdRead.Flags.IntVar(&numEntries, "n", int(logtypes.AllEntries), "The number of log entries to read.")
+	cmdRead.Flags.Int64Var(&startPos, "o", 0, "The position, in bytes, from which to start reading the log file.")
+
+	// stats value flags
+	cmdValue.Flags.BoolVar(&raw, "raw", false, "When true, the command will display the raw value of the object.")
+	cmdValue.Flags.BoolVar(&showType, "type", false, "When true, the type of the values will be displayed.")
+
+	// stats watchglob flags
+	cmdWatchGlob.Flags.BoolVar(&raw, "raw", false, "When true, the command will display the raw value of the object.")
+	cmdWatchGlob.Flags.BoolVar(&showType, "type", false, "When true, the type of the values will be displayed.")
+
+	// pprof flags
+	cmdPProfRun.Flags.StringVar(&pprofCmd, "pprofcmd", "veyron go tool pprof", "The pprof command to use.")
+}
+
+var cmdGlob = &cmdline.Command{
+	Run:      runGlob,
+	Name:     "glob",
+	Short:    "Returns all matching entries from the namespace",
+	Long:     "Returns all matching entries from the namespace.",
+	ArgsName: "<pattern> ...",
+	ArgsLong: `
+<pattern> is a glob pattern to match.
+`,
+}
+
+func runGlob(cmd *cmdline.Command, args []string) error {
+	if want, got := 1, len(args); got < want {
+		return cmd.UsageErrorf("glob: incorrect number of arguments, got %d, want >=%d", got, want)
+	}
+	results := make(chan mttypes.MountEntry)
+	errors := make(chan error)
+	ctx := rt.R().NewContext()
+	for _, a := range args {
+		go doGlob(ctx, a, results, errors)
+	}
+	var lastErr error
+	count := 0
+L:
+	for {
+		select {
+		case r := <-results:
+			fmt.Fprint(cmd.Stdout(), r.Name)
+			for _, s := range r.Servers {
+				fmt.Fprintf(cmd.Stdout(), " %s (TTL %s)", s.Server, s.TTL)
+			}
+			fmt.Fprintln(cmd.Stdout())
+		case e := <-errors:
+			if e != nil {
+				lastErr = e
+				fmt.Fprintln(cmd.Stderr(), e)
+			}
+			count++
+			if count == len(args) {
+				break L
+			}
+		}
+	}
+	return lastErr
+}
+
+func doGlob(ctx context.T, pattern string, results chan<- mttypes.MountEntry, errors chan<- error) {
+	// TODO(rthellend): Consider using the namespace library.
+	root, globPattern := naming.SplitAddressName(pattern)
+	g, err := glob.Parse(globPattern)
+	if err != nil {
+		errors <- fmt.Errorf("glob.Parse(%s): %v", globPattern, err)
+		return
+	}
+	var prefixElems []string
+	prefixElems, g = g.SplitFixedPrefix()
+	name := naming.Join(prefixElems...)
+	if len(root) != 0 {
+		name = naming.JoinAddressName(root, name)
+	}
+	c, err := mounttable.BindGlobbable(name)
+	if err != nil {
+		errors <- err
+		return
+	}
+	stream, err := c.Glob(ctx, g.String())
+	if err != nil {
+		errors <- err
+		return
+	}
+	iterator := stream.RecvStream()
+	for iterator.Advance() {
+		me := iterator.Value()
+		me.Name = naming.Join(name, me.Name)
+		results <- me
+	}
+	if err := iterator.Err(); err != nil {
+		errors <- err
+		return
+	}
+	if err = stream.Finish(); err != nil {
+		errors <- err
+		return
+	}
+	errors <- nil
+}
+
+var cmdRead = &cmdline.Command{
+	Run:      runRead,
+	Name:     "read",
+	Short:    "Reads the content of a log file object.",
+	Long:     "Reads the content of a log file object.",
+	ArgsName: "<name>",
+	ArgsLong: `
+<name> is the name of the log file object.
+`,
+}
+
+func runRead(cmd *cmdline.Command, args []string) error {
+	if want, got := 1, len(args); want != got {
+		return cmd.UsageErrorf("read: incorrect number of arguments, got %d, want %d", got, want)
+	}
+	name := args[0]
+	lf, err := logreader.BindLogFile(name)
+	if err != nil {
+		return err
+	}
+	stream, err := lf.ReadLog(rt.R().NewContext(), startPos, int32(numEntries), follow)
+	if err != nil {
+		return err
+	}
+	iterator := stream.RecvStream()
+	for iterator.Advance() {
+		entry := iterator.Value()
+		if verbose {
+			fmt.Fprintf(cmd.Stdout(), "[%d] %s\n", entry.Position, entry.Line)
+		} else {
+			fmt.Fprintf(cmd.Stdout(), "%s\n", entry.Line)
+		}
+	}
+	if err = iterator.Err(); err != nil {
+		return err
+	}
+	offset, err := stream.Finish()
+	if err != nil {
+		return err
+	}
+	if verbose {
+		fmt.Fprintf(cmd.Stdout(), "Offset: %d\n", offset)
+	}
+	return nil
+}
+
+var cmdSize = &cmdline.Command{
+	Run:      runSize,
+	Name:     "size",
+	Short:    "Returns the size of the a log file object",
+	Long:     "Returns the size of the a log file object.",
+	ArgsName: "<name>",
+	ArgsLong: `
+<name> is the name of the log file object.
+`,
+}
+
+func runSize(cmd *cmdline.Command, args []string) error {
+	if want, got := 1, len(args); want != got {
+		return cmd.UsageErrorf("size: incorrect number of arguments, got %d, want %d", got, want)
+	}
+	name := args[0]
+	lf, err := logreader.BindLogFile(name)
+	if err != nil {
+		return err
+	}
+	size, err := lf.Size(rt.R().NewContext())
+	if err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), size)
+	return nil
+}
+
+var cmdValue = &cmdline.Command{
+	Run:      runValue,
+	Name:     "value",
+	Short:    "Returns the value of the a stats object",
+	Long:     "Returns the value of the a stats object.",
+	ArgsName: "<name>",
+	ArgsLong: `
+<name> is the name of the stats object.
+`,
+}
+
+func runValue(cmd *cmdline.Command, args []string) error {
+	if want, got := 1, len(args); want != got {
+		return cmd.UsageErrorf("value: incorrect number of arguments, got %d, want %d", got, want)
+	}
+	name := args[0]
+	lf, err := stats.BindStats(name)
+	if err != nil {
+		return err
+	}
+	v, err := lf.Value(rt.R().NewContext())
+	if err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), formatValue(v))
+	return nil
+}
+
+var cmdWatchGlob = &cmdline.Command{
+	Run:      runWatchGlob,
+	Name:     "watchglob",
+	Short:    "Returns a stream of all matching entries and their values",
+	Long:     "Returns a stream of all matching entries and their values",
+	ArgsName: "<pattern> ...",
+	ArgsLong: `
+<pattern> is a glob pattern to match.
+`,
+}
+
+func runWatchGlob(cmd *cmdline.Command, args []string) error {
+	if want, got := 1, len(args); got < want {
+		return cmd.UsageErrorf("watchglob: incorrect number of arguments, got %d, want >=%d", got, want)
+	}
+
+	results := make(chan string)
+	errors := make(chan error)
+	ctx := rt.R().NewContext()
+	for _, a := range args {
+		go doWatchGlob(ctx, a, results, errors)
+	}
+	var lastErr error
+	count := 0
+L:
+	for {
+		select {
+		case r := <-results:
+			fmt.Fprintln(cmd.Stdout(), r)
+		case e := <-errors:
+			if e != nil {
+				lastErr = e
+				fmt.Fprintln(cmd.Stderr(), e)
+			}
+			count++
+			if count == len(args) {
+				break L
+			}
+		}
+	}
+	return lastErr
+}
+
+func doWatchGlob(ctx context.T, pattern string, results chan<- string, errors chan<- error) {
+	root, globPattern := naming.SplitAddressName(pattern)
+	g, err := glob.Parse(globPattern)
+	if err != nil {
+		errors <- fmt.Errorf("glob.Parse(%s): %v", globPattern, err)
+		return
+	}
+	var prefixElems []string
+	prefixElems, g = g.SplitFixedPrefix()
+	name := naming.Join(prefixElems...)
+	if len(root) != 0 {
+		name = naming.JoinAddressName(root, name)
+	}
+	c, err := watch.BindGlobWatcher(name)
+	if err != nil {
+		errors <- err
+		return
+	}
+	stream, err := c.WatchGlob(ctx, watchtypes.GlobRequest{Pattern: g.String()})
+	if err != nil {
+		errors <- err
+		return
+	}
+	iterator := stream.RecvStream()
+	for iterator.Advance() {
+		v := iterator.Value()
+		results <- fmt.Sprintf("%s: %s", naming.Join(name, v.Name), formatValue(v.Value))
+	}
+	if err = iterator.Err(); err != nil {
+		errors <- err
+		return
+	}
+	if err = stream.Finish(); err != nil {
+		errors <- err
+	}
+	errors <- nil
+}
+
+func formatValue(value interface{}) string {
+	var buf bytes.Buffer
+	if showType {
+		fmt.Fprintf(&buf, "%T: ", value)
+	}
+	if raw {
+		fmt.Fprintf(&buf, "%+v", value)
+		return buf.String()
+	}
+	switch v := value.(type) {
+	case istats.HistogramValue:
+		writeASCIIHistogram(&buf, v)
+	default:
+		fmt.Fprintf(&buf, "%v", v)
+	}
+	return buf.String()
+}
+
+func writeASCIIHistogram(w io.Writer, h istats.HistogramValue) {
+	scale := h.Count
+	if scale > 100 {
+		scale = 100
+	}
+	var avg float64
+	if h.Count > 0 {
+		avg = float64(h.Sum) / float64(h.Count)
+	}
+	fmt.Fprintf(w, "Count: %d Sum: %d Avg: %f\n", h.Count, h.Sum, avg)
+	for i, b := range h.Buckets {
+		var r string
+		if i+1 < len(h.Buckets) {
+			r = fmt.Sprintf("[%d,%d[", b.LowBound, h.Buckets[i+1].LowBound)
+		} else {
+			r = fmt.Sprintf("[%d,Inf", b.LowBound)
+		}
+		fmt.Fprintf(w, "%-18s: ", r)
+		if b.Count > 0 && h.Count > 0 {
+			fmt.Fprintf(w, "%s %d (%.1f%%)", strings.Repeat("*", int(b.Count*scale/h.Count)), b.Count, 100*float64(b.Count)/float64(h.Count))
+		}
+		if i+1 < len(h.Buckets) {
+			fmt.Fprintln(w)
+		}
+	}
+}
+
+var cmdPProfRun = &cmdline.Command{
+	Run:      runPProf,
+	Name:     "run",
+	Short:    "Runs the pprof tool",
+	Long:     "Runs the pprof tool",
+	ArgsName: "<name> <profile> [passthru args] ...",
+	ArgsLong: `
+<name> is the name of the pprof object.
+<profile> the name of the profile to use.
+
+All the [passthru args] are passed to the pprof tool directly, e.g.
+
+$ debug pprof run a/b/c heap --text --lines
+$ debug pprof run a/b/c profile -gv
+`,
+}
+
+func runPProf(cmd *cmdline.Command, args []string) error {
+	if min, got := 1, len(args); got < min {
+		return cmd.UsageErrorf("pprof: incorrect number of arguments, got %d, want >=%d", got, min)
+	}
+	name := args[0]
+	if len(args) == 1 {
+		return showPProfProfiles(cmd, name)
+	}
+	profile := args[1]
+	listener, err := client.StartProxy(rt.R(), name)
+	if err != nil {
+		return err
+	}
+	defer listener.Close()
+
+	// Construct the pprof command line:
+	// <pprofCmd> http://<proxyaddr>/pprof/<profile> [pprof flags]
+	pargs := []string{pprofCmd} // pprofCmd is purposely not escaped.
+	for i := 2; i < len(args); i++ {
+		pargs = append(pargs, shellEscape(args[i]))
+	}
+	pargs = append(pargs, shellEscape(fmt.Sprintf("http://%s/pprof/%s", listener.Addr(), profile)))
+	pcmd := strings.Join(pargs, " ")
+	fmt.Fprintf(cmd.Stdout(), "Running: %s\n", pcmd)
+	c := exec.Command("sh", "-c", pcmd)
+	c.Stdin = os.Stdin
+	c.Stdout = cmd.Stdout()
+	c.Stderr = cmd.Stderr()
+	return c.Run()
+}
+
+func showPProfProfiles(cmd *cmdline.Command, name string) error {
+	pp, err := pprof.BindPProf(name)
+	if err != nil {
+		return err
+	}
+	v, err := pp.Profiles(rt.R().NewContext())
+	if err != nil {
+		return err
+	}
+	v = append(v, "profile")
+	sort.Strings(v)
+	fmt.Fprintln(cmd.Stdout(), "Available profiles:")
+	for _, p := range v {
+		fmt.Fprintf(cmd.Stdout(), "  %s\n", p)
+	}
+	return nil
+}
+
+func shellEscape(s string) string {
+	if !strings.Contains(s, "'") {
+		return "'" + s + "'"
+	}
+	re := regexp.MustCompile("([\"$`\\\\])")
+	return `"` + re.ReplaceAllString(s, "\\$1") + `"`
+}
+
+var cmdPProfRunProxy = &cmdline.Command{
+	Run:      runPProfProxy,
+	Name:     "proxy",
+	Short:    "Runs an http proxy to a pprof object",
+	Long:     "Runs an http proxy to a pprof object",
+	ArgsName: "<name>",
+	ArgsLong: `
+<name> is the name of the pprof object.
+`,
+}
+
+func runPProfProxy(cmd *cmdline.Command, args []string) error {
+	if want, got := 1, len(args); got != want {
+		return cmd.UsageErrorf("proxy: incorrect number of arguments, got %d, want %d", got, want)
+	}
+	name := args[0]
+	listener, err := client.StartProxy(rt.R(), name)
+	if err != nil {
+		return err
+	}
+	defer listener.Close()
+
+	fmt.Fprintln(cmd.Stdout())
+	fmt.Fprintf(cmd.Stdout(), "The pprof proxy is listening at http://%s/pprof\n", listener.Addr())
+	fmt.Fprintln(cmd.Stdout())
+	fmt.Fprintln(cmd.Stdout(), "Hit CTRL-C to exit")
+
+	<-signals.ShutdownOnSignals()
+	return nil
+}
+
+var cmdRoot = cmdline.Command{
+	Name:  "debug",
+	Short: "Command-line tool for interacting with the debug server",
+	Long:  "Command-line tool for interacting with the debug server.",
+	Children: []*cmdline.Command{
+		cmdGlob,
+		&cmdline.Command{
+			Name:     "logs",
+			Short:    "Accesses log files",
+			Long:     "Accesses log files",
+			Children: []*cmdline.Command{cmdRead, cmdSize},
+		},
+		&cmdline.Command{
+			Name:     "stats",
+			Short:    "Accesses stats",
+			Long:     "Accesses stats",
+			Children: []*cmdline.Command{cmdValue, cmdWatchGlob},
+		},
+		&cmdline.Command{
+			Name:     "pprof",
+			Short:    "Accesses profiling data",
+			Long:     "Accesses profiling data",
+			Children: []*cmdline.Command{cmdPProfRun, cmdPProfRunProxy},
+		},
+	},
+}
+
+func Root() *cmdline.Command {
+	return &cmdRoot
+}