github.com/shrimpyuk/bor@v0.2.15-0.20220224151350-fb4ec6020bae/internal/cli/debug.go (about)

     1  package cli
     2  
     3  // Based on https://github.com/hashicorp/nomad/blob/main/command/operator_debug.go
     4  
     5  import (
     6  	"archive/tar"
     7  	"compress/gzip"
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  	"os/signal"
    14  	"path/filepath"
    15  	"strings"
    16  	"syscall"
    17  	"time"
    18  
    19  	"github.com/ethereum/go-ethereum/internal/cli/flagset"
    20  	"github.com/ethereum/go-ethereum/internal/cli/server/proto"
    21  	"github.com/golang/protobuf/jsonpb"
    22  	gproto "github.com/golang/protobuf/proto"
    23  	"github.com/golang/protobuf/ptypes/empty"
    24  	grpc_net_conn "github.com/mitchellh/go-grpc-net-conn"
    25  )
    26  
    27  type DebugCommand struct {
    28  	*Meta2
    29  
    30  	seconds uint64
    31  	output  string
    32  }
    33  
    34  // Help implements the cli.Command interface
    35  func (d *DebugCommand) Help() string {
    36  	return `Usage: bor debug
    37  
    38    Build an archive containing Bor pprof traces
    39  
    40    ` + d.Flags().Help()
    41  }
    42  
    43  func (d *DebugCommand) Flags() *flagset.Flagset {
    44  	flags := d.NewFlagSet("debug")
    45  
    46  	flags.Uint64Flag(&flagset.Uint64Flag{
    47  		Name:    "seconds",
    48  		Usage:   "seconds to trace",
    49  		Value:   &d.seconds,
    50  		Default: 2,
    51  	})
    52  	flags.StringFlag(&flagset.StringFlag{
    53  		Name:  "output",
    54  		Value: &d.output,
    55  		Usage: "Output directory",
    56  	})
    57  
    58  	return flags
    59  }
    60  
    61  // Synopsis implements the cli.Command interface
    62  func (d *DebugCommand) Synopsis() string {
    63  	return "Build an archive containing Bor pprof traces"
    64  }
    65  
    66  // Run implements the cli.Command interface
    67  func (d *DebugCommand) Run(args []string) int {
    68  	flags := d.Flags()
    69  	if err := flags.Parse(args); err != nil {
    70  		d.UI.Error(err.Error())
    71  		return 1
    72  	}
    73  
    74  	clt, err := d.BorConn()
    75  	if err != nil {
    76  		d.UI.Error(err.Error())
    77  		return 1
    78  	}
    79  
    80  	stamped := "bor-debug-" + time.Now().UTC().Format("2006-01-02-150405Z")
    81  
    82  	// Create the output directory
    83  	var tmp string
    84  	if d.output != "" {
    85  		// User specified output directory
    86  		tmp = filepath.Join(d.output, stamped)
    87  		_, err := os.Stat(tmp)
    88  		if !os.IsNotExist(err) {
    89  			d.UI.Error("Output directory already exists")
    90  			return 1
    91  		}
    92  	} else {
    93  		// Generate temp directory
    94  		tmp, err = ioutil.TempDir(os.TempDir(), stamped)
    95  		if err != nil {
    96  			d.UI.Error(fmt.Sprintf("Error creating tmp directory: %s", err.Error()))
    97  			return 1
    98  		}
    99  		defer os.RemoveAll(tmp)
   100  	}
   101  
   102  	d.UI.Output("Starting debugger...")
   103  	d.UI.Output("")
   104  
   105  	// ensure destine folder exists
   106  	if err := os.MkdirAll(tmp, os.ModePerm); err != nil {
   107  		d.UI.Error(fmt.Sprintf("failed to create parent directory: %v", err))
   108  		return 1
   109  	}
   110  
   111  	pprofProfile := func(ctx context.Context, profile string, filename string) error {
   112  		req := &proto.PprofRequest{
   113  			Seconds: int64(d.seconds),
   114  		}
   115  		switch profile {
   116  		case "cpu":
   117  			req.Type = proto.PprofRequest_CPU
   118  		case "trace":
   119  			req.Type = proto.PprofRequest_TRACE
   120  		default:
   121  			req.Type = proto.PprofRequest_LOOKUP
   122  			req.Profile = profile
   123  		}
   124  		stream, err := clt.Pprof(ctx, req)
   125  		if err != nil {
   126  			return err
   127  		}
   128  		// wait for open request
   129  		msg, err := stream.Recv()
   130  		if err != nil {
   131  			return err
   132  		}
   133  		if _, ok := msg.Event.(*proto.PprofResponse_Open_); !ok {
   134  			return fmt.Errorf("expected open message")
   135  		}
   136  
   137  		// create the stream
   138  		conn := &grpc_net_conn.Conn{
   139  			Stream:   stream,
   140  			Response: &proto.PprofResponse_Input{},
   141  			Decode: grpc_net_conn.SimpleDecoder(func(msg gproto.Message) *[]byte {
   142  				return &msg.(*proto.PprofResponse_Input).Data
   143  			}),
   144  		}
   145  
   146  		file, err := os.OpenFile(filepath.Join(tmp, filename+".prof"), os.O_RDWR|os.O_CREATE, 0644)
   147  		if err != nil {
   148  			return err
   149  		}
   150  		defer file.Close()
   151  
   152  		if _, err := io.Copy(file, conn); err != nil {
   153  			return err
   154  		}
   155  		return nil
   156  	}
   157  
   158  	ctx, cancelFn := context.WithCancel(context.Background())
   159  	trapSignal(cancelFn)
   160  
   161  	profiles := map[string]string{
   162  		"heap":  "heap",
   163  		"cpu":   "cpu",
   164  		"trace": "trace",
   165  	}
   166  	for profile, filename := range profiles {
   167  		if err := pprofProfile(ctx, profile, filename); err != nil {
   168  			d.UI.Error(fmt.Sprintf("Error creating profile '%s': %v", profile, err))
   169  			return 1
   170  		}
   171  	}
   172  
   173  	// append the status
   174  	{
   175  		statusResp, err := clt.Status(ctx, &empty.Empty{})
   176  		if err != nil {
   177  			d.UI.Output(fmt.Sprintf("Failed to get status: %v", err))
   178  			return 1
   179  		}
   180  		m := jsonpb.Marshaler{}
   181  		data, err := m.MarshalToString(statusResp)
   182  		if err != nil {
   183  			d.UI.Output(err.Error())
   184  			return 1
   185  		}
   186  		if err := ioutil.WriteFile(filepath.Join(tmp, "status.json"), []byte(data), 0644); err != nil {
   187  			d.UI.Output(fmt.Sprintf("Failed to write status: %v", err))
   188  			return 1
   189  		}
   190  	}
   191  
   192  	// Exit before archive if output directory was specified
   193  	if d.output != "" {
   194  		d.UI.Output(fmt.Sprintf("Created debug directory: %s", tmp))
   195  		return 0
   196  	}
   197  
   198  	// Create archive tarball
   199  	archiveFile := stamped + ".tar.gz"
   200  	if err = tarCZF(archiveFile, tmp, stamped); err != nil {
   201  		d.UI.Error(fmt.Sprintf("Error creating archive: %s", err.Error()))
   202  		return 1
   203  	}
   204  
   205  	d.UI.Output(fmt.Sprintf("Created debug archive: %s", archiveFile))
   206  	return 0
   207  }
   208  
   209  func trapSignal(cancel func()) {
   210  	sigCh := make(chan os.Signal, 1)
   211  	signal.Notify(sigCh,
   212  		syscall.SIGHUP,
   213  		syscall.SIGINT,
   214  		syscall.SIGTERM,
   215  		syscall.SIGQUIT)
   216  
   217  	go func() {
   218  		<-sigCh
   219  		cancel()
   220  	}()
   221  }
   222  
   223  func tarCZF(archive string, src, target string) error {
   224  	// ensure the src actually exists before trying to tar it
   225  	if _, err := os.Stat(src); err != nil {
   226  		return fmt.Errorf("unable to tar files - %v", err.Error())
   227  	}
   228  
   229  	// create the archive
   230  	fh, err := os.Create(archive)
   231  	if err != nil {
   232  		return err
   233  	}
   234  	defer fh.Close()
   235  
   236  	zz := gzip.NewWriter(fh)
   237  	defer zz.Close()
   238  
   239  	tw := tar.NewWriter(zz)
   240  	defer tw.Close()
   241  
   242  	// tar
   243  	return filepath.Walk(src, func(file string, fi os.FileInfo, err error) error {
   244  		// return on any error
   245  		if err != nil {
   246  			return err
   247  		}
   248  		if !fi.Mode().IsRegular() {
   249  			return nil
   250  		}
   251  
   252  		header, err := tar.FileInfoHeader(fi, fi.Name())
   253  		if err != nil {
   254  			return err
   255  		}
   256  
   257  		// remove leading path to the src, so files are relative to the archive
   258  		path := strings.ReplaceAll(file, src, "")
   259  		if target != "" {
   260  			path = filepath.Join([]string{target, path}...)
   261  		}
   262  		path = strings.TrimPrefix(path, string(filepath.Separator))
   263  
   264  		header.Name = path
   265  
   266  		if err := tw.WriteHeader(header); err != nil {
   267  			return err
   268  		}
   269  
   270  		// copy the file contents
   271  		f, err := os.Open(file)
   272  		if err != nil {
   273  			return err
   274  		}
   275  
   276  		if _, err := io.Copy(tw, f); err != nil {
   277  			return err
   278  		}
   279  
   280  		f.Close()
   281  		return nil
   282  	})
   283  }