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 }