go-ethereum/internal/cli/debug.go
Manav Darji 0d2b1d0630
internal/cli: add block tracing (#397)
* bad block tracing

* Add debug env framework

* add implementation of custom block tracing

* add few comments

* use tar name fn

* wip: add unit tests for block tracing

* complete test for block tracer

* fix: close grpc server

* refactor cli tests

* fix: change condition for parsing args

* Fix port binding for test server

* add helper for creating and closing mock server for tests

* consume mock server in tests

* fixes due to geth merge

* fix: handle port selection for http server

* update help and markdown for debug command

* update docs

* update debug synopsis

* fix: use chunked encoder to handle large data over grpc

* fix prints

* lint

* rm unused function, rename fn to TraceBorBlock

Co-authored-by: Ferran Borreguero <ferranbt@protonmail.com>
2022-06-06 14:54:05 +05:30

273 lines
5.7 KiB
Go

package cli
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/mitchellh/cli"
"github.com/ethereum/go-ethereum/internal/cli/server/proto"
"github.com/golang/protobuf/jsonpb" // nolint:staticcheck
gproto "github.com/golang/protobuf/proto" // nolint:staticcheck
grpc_net_conn "github.com/mitchellh/go-grpc-net-conn"
"google.golang.org/grpc"
"google.golang.org/protobuf/runtime/protoiface"
)
// DebugCommand is the command to group the peers commands
type DebugCommand struct {
UI cli.Ui
}
// MarkDown implements cli.MarkDown interface
func (d *DebugCommand) MarkDown() string {
examples := []string{
"## Examples",
"By default it creates a tar.gz file with the output:",
CodeBlock([]string{
"$ bor debug",
"Starting debugger...\n",
"Created debug archive: bor-debug-2021-10-26-073819Z.tar.gz",
}),
"Send the output to a specific directory:",
CodeBlock([]string{
"$ bor debug --output data",
"Starting debugger...\n",
"Created debug directory: data/bor-debug-2021-10-26-075437Z",
}),
}
items := []string{
"# Debug",
"The ```bor debug``` command takes a debug dump of the running client.",
"- [```bor debug pprof```](./debug_pprof.md): Dumps bor pprof traces.",
"- [```bor debug block <number>```](./debug_block.md): Dumps bor block traces.",
}
items = append(items, examples...)
return strings.Join(items, "\n\n")
}
// Help implements the cli.Command interface
func (c *DebugCommand) Help() string {
return `Usage: bor debug <subcommand>
This command takes a debug dump of the running client.
Get the pprof traces:
$ bor debug pprof <enode>
Get the block traces:
$ bor debug block <number>`
}
// Synopsis implements the cli.Command interface
func (c *DebugCommand) Synopsis() string {
return "Get traces of the running client"
}
// Run implements the cli.Command interface
func (c *DebugCommand) Run(args []string) int {
return cli.RunResultHelp
}
type debugEnv struct {
output string
prefix string
name string
dst string
}
func (d *debugEnv) init() error {
d.name = d.prefix + time.Now().UTC().Format("2006-01-02-150405Z")
var err error
// Create the output directory
var tmp string
if d.output != "" {
// User specified output directory
tmp = filepath.Join(d.output, d.name)
_, err := os.Stat(tmp)
if !os.IsNotExist(err) {
return fmt.Errorf("output directory already exists")
}
} else {
// Generate temp directory
tmp, err = ioutil.TempDir(os.TempDir(), d.name)
if err != nil {
return fmt.Errorf("error creating tmp directory: %s", err.Error())
}
}
// ensure destine folder exists
if err := os.MkdirAll(tmp, os.ModePerm); err != nil {
return fmt.Errorf("failed to create parent directory: %v", err)
}
d.dst = tmp
return nil
}
func (d *debugEnv) tarName() string {
return d.name + ".tar.gz"
}
func (d *debugEnv) finish() error {
// Exit before archive if output directory was specified
if d.output != "" {
return nil
}
// Create archive tarball
archiveFile := d.tarName()
if err := tarCZF(archiveFile, d.dst, d.name); err != nil {
return fmt.Errorf("error creating archive: %s", err.Error())
}
return nil
}
type debugStream interface {
Recv() (*proto.DebugFileResponse, error)
grpc.ClientStream
}
func (d *debugEnv) writeFromStream(name string, stream debugStream) error {
// wait for open request
msg, err := stream.Recv()
if err != nil {
return err
}
if _, ok := msg.Event.(*proto.DebugFileResponse_Open_); !ok {
return fmt.Errorf("expected open message")
}
// create the stream
conn := &grpc_net_conn.Conn{
Stream: stream,
Response: &proto.DebugFileResponse_Input{},
Decode: grpc_net_conn.SimpleDecoder(func(msg gproto.Message) *[]byte {
return &msg.(*proto.DebugFileResponse_Input).Data
}),
}
file, err := os.OpenFile(filepath.Join(d.dst, name), os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return err
}
defer file.Close()
if _, err := io.Copy(file, conn); err != nil {
return err
}
return nil
}
func (d *debugEnv) writeJSON(name string, msg protoiface.MessageV1) error {
m := jsonpb.Marshaler{}
data, err := m.MarshalToString(msg)
if err != nil {
return err
}
if err := ioutil.WriteFile(filepath.Join(d.dst, name), []byte(data), 0600); err != nil {
return fmt.Errorf("failed to write status: %v", err)
}
return nil
}
func trapSignal(cancel func()) {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT)
go func() {
<-sigCh
cancel()
}()
}
func tarCZF(archive string, src, target string) error {
// ensure the src actually exists before trying to tar it
if _, err := os.Stat(src); err != nil {
return fmt.Errorf("unable to tar files - %v", err.Error())
}
// create the archive
fh, err := os.Create(archive)
if err != nil {
return err
}
defer fh.Close()
zz := gzip.NewWriter(fh)
defer zz.Close()
tw := tar.NewWriter(zz)
defer tw.Close()
// tar
return filepath.Walk(src, func(file string, fi os.FileInfo, err error) error {
// return on any error
if err != nil {
return err
}
if !fi.Mode().IsRegular() {
return nil
}
header, err := tar.FileInfoHeader(fi, fi.Name())
if err != nil {
return err
}
// remove leading path to the src, so files are relative to the archive
path := strings.ReplaceAll(file, src, "")
if target != "" {
path = filepath.Join([]string{target, path}...)
}
path = strings.TrimPrefix(path, string(filepath.Separator))
header.Name = path
if err := tw.WriteHeader(header); err != nil {
return err
}
// copy the file contents
f, err := os.Open(file)
if err != nil {
return err
}
if _, err := io.Copy(tw, f); err != nil {
return err
}
f.Close()
return nil
})
}