github.com/undoio/delve@v1.9.0/pkg/proc/gdbserial/rr.go (about)

     1  package gdbserial
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"strconv"
    12  	"strings"
    13  	"syscall"
    14  
    15  	"github.com/undoio/delve/pkg/config"
    16  	"github.com/undoio/delve/pkg/proc"
    17  )
    18  
    19  // RecordAsync configures rr to record the execution of the specified
    20  // program. Returns a run function which will actually record the program, a
    21  // stop function which will prematurely terminate the recording of the
    22  // program.
    23  func RecordAsync(cmd []string, wd string, quiet bool, redirects [3]string) (run func() (string, error), stop func() error, err error) {
    24  	if err := checkRRAvailable(); err != nil {
    25  		return nil, nil, err
    26  	}
    27  
    28  	rfd, wfd, err := os.Pipe()
    29  	if err != nil {
    30  		return nil, nil, err
    31  	}
    32  
    33  	args := make([]string, 0, len(cmd)+2)
    34  	args = append(args, "record", "--print-trace-dir=3")
    35  	args = append(args, cmd...)
    36  	rrcmd := exec.Command("rr", args...)
    37  	var closefn func()
    38  	rrcmd.Stdin, rrcmd.Stdout, rrcmd.Stderr, closefn, err = openRedirects(redirects, quiet)
    39  	if err != nil {
    40  		return nil, nil, err
    41  	}
    42  	rrcmd.ExtraFiles = []*os.File{wfd}
    43  	rrcmd.Dir = wd
    44  
    45  	tracedirChan := make(chan string)
    46  	go func() {
    47  		bs, _ := ioutil.ReadAll(rfd)
    48  		tracedirChan <- strings.TrimSpace(string(bs))
    49  	}()
    50  
    51  	run = func() (string, error) {
    52  		err := rrcmd.Run()
    53  		closefn()
    54  		_ = wfd.Close()
    55  		tracedir := <-tracedirChan
    56  		return tracedir, err
    57  	}
    58  
    59  	stop = func() error {
    60  		return rrcmd.Process.Signal(syscall.SIGTERM)
    61  	}
    62  
    63  	return run, stop, nil
    64  }
    65  
    66  func openRedirects(redirects [3]string, quiet bool) (stdin, stdout, stderr *os.File, closefn func(), err error) {
    67  	toclose := []*os.File{}
    68  
    69  	if redirects[0] != "" {
    70  		stdin, err = os.Open(redirects[0])
    71  		if err != nil {
    72  			return nil, nil, nil, nil, err
    73  		}
    74  		toclose = append(toclose, stdin)
    75  	} else {
    76  		stdin = os.Stdin
    77  	}
    78  
    79  	create := func(path string, dflt *os.File) *os.File {
    80  		if path == "" {
    81  			if quiet {
    82  				return nil
    83  			}
    84  			return dflt
    85  		}
    86  		var f *os.File
    87  		f, err = os.Create(path)
    88  		if f != nil {
    89  			toclose = append(toclose, f)
    90  		}
    91  		return f
    92  	}
    93  
    94  	stdout = create(redirects[1], os.Stdout)
    95  	if err != nil {
    96  		return nil, nil, nil, nil, err
    97  	}
    98  
    99  	stderr = create(redirects[2], os.Stderr)
   100  	if err != nil {
   101  		return nil, nil, nil, nil, err
   102  	}
   103  
   104  	closefn = func() {
   105  		for _, f := range toclose {
   106  			_ = f.Close()
   107  		}
   108  	}
   109  
   110  	return stdin, stdout, stderr, closefn, nil
   111  }
   112  
   113  // Record uses rr to record the execution of the specified program and
   114  // returns the trace directory's path.
   115  func Record(cmd []string, wd string, quiet bool, redirects [3]string) (tracedir string, err error) {
   116  	run, _, err := RecordAsync(cmd, wd, quiet, redirects)
   117  	if err != nil {
   118  		return "", err
   119  	}
   120  
   121  	// ignore run errors, it could be the program crashing
   122  	return run()
   123  }
   124  
   125  // Replay starts an instance of rr in replay mode, with the specified trace
   126  // directory, and connects to it.
   127  func Replay(tracedir string, quiet, deleteOnDetach bool, debugInfoDirs []string) (*proc.Target, error) {
   128  	if err := checkRRAvailable(); err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	rrcmd := exec.Command("rr", "replay", "--dbgport=0", tracedir)
   133  	rrcmd.Stdout = os.Stdout
   134  	stderr, err := rrcmd.StderrPipe()
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	rrcmd.SysProcAttr = sysProcAttr(false)
   139  
   140  	initch := make(chan rrInit)
   141  	go rrStderrParser(stderr, initch, quiet)
   142  
   143  	err = rrcmd.Start()
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	init := <-initch
   149  	if init.err != nil {
   150  		rrcmd.Process.Kill()
   151  		return nil, init.err
   152  	}
   153  
   154  	p := newProcess(rrcmd.Process)
   155  	p.tracedir = tracedir
   156  	p.conn.useXcmd = true // 'rr' does not support the 'M' command which is what we would usually use to write memory, this is only important during function calls, in any other situation writing memory will fail anyway.
   157  	if deleteOnDetach {
   158  		p.onDetach = func() {
   159  			safeRemoveAll(p.tracedir)
   160  		}
   161  	}
   162  	tgt, err := p.Dial(init.port, init.exe, 0, debugInfoDirs, proc.StopLaunched)
   163  	if err != nil {
   164  		rrcmd.Process.Kill()
   165  		return nil, err
   166  	}
   167  
   168  	return tgt, nil
   169  }
   170  
   171  // ErrPerfEventParanoid is the error returned by Reply and Record if
   172  // /proc/sys/kernel/perf_event_paranoid is greater than 1.
   173  type ErrPerfEventParanoid struct {
   174  	actual int
   175  }
   176  
   177  func (err ErrPerfEventParanoid) Error() string {
   178  	return fmt.Sprintf("rr needs /proc/sys/kernel/perf_event_paranoid <= 1, but it is %d", err.actual)
   179  }
   180  
   181  func checkRRAvailable() error {
   182  	if _, err := exec.LookPath("rr"); err != nil {
   183  		return &ErrBackendUnavailable{}
   184  	}
   185  
   186  	// Check that /proc/sys/kernel/perf_event_paranoid doesn't exist or is <= 1.
   187  	buf, err := ioutil.ReadFile("/proc/sys/kernel/perf_event_paranoid")
   188  	if err == nil {
   189  		perfEventParanoid, _ := strconv.Atoi(strings.TrimSpace(string(buf)))
   190  		if perfEventParanoid > 1 {
   191  			return ErrPerfEventParanoid{perfEventParanoid}
   192  		}
   193  	}
   194  
   195  	return nil
   196  }
   197  
   198  type rrInit struct {
   199  	port string
   200  	exe  string
   201  	err  error
   202  }
   203  
   204  const (
   205  	rrGdbCommandPrefix = "  gdb "
   206  	rrGdbLaunchPrefix  = "Launch gdb with"
   207  	targetCmd          = "target extended-remote "
   208  )
   209  
   210  func rrStderrParser(stderr io.ReadCloser, initch chan<- rrInit, quiet bool) {
   211  	rd := bufio.NewReader(stderr)
   212  	defer stderr.Close()
   213  
   214  	for {
   215  		line, err := rd.ReadString('\n')
   216  		if err != nil {
   217  			initch <- rrInit{"", "", err}
   218  			close(initch)
   219  			return
   220  		}
   221  
   222  		if strings.HasPrefix(line, rrGdbCommandPrefix) {
   223  			initch <- rrParseGdbCommand(line[len(rrGdbCommandPrefix):])
   224  			close(initch)
   225  			break
   226  		}
   227  
   228  		if strings.HasPrefix(line, rrGdbLaunchPrefix) {
   229  			continue
   230  		}
   231  
   232  		if !quiet {
   233  			os.Stderr.Write([]byte(line))
   234  		}
   235  	}
   236  
   237  	io.Copy(os.Stderr, rd)
   238  }
   239  
   240  type ErrMalformedRRGdbCommand struct {
   241  	line, reason string
   242  }
   243  
   244  func (err *ErrMalformedRRGdbCommand) Error() string {
   245  	return fmt.Sprintf("malformed gdb command %q: %s", err.line, err.reason)
   246  }
   247  
   248  func rrParseGdbCommand(line string) rrInit {
   249  	port := ""
   250  	fields := config.SplitQuotedFields(line, '\'')
   251  	for i := 0; i < len(fields); i++ {
   252  		switch fields[i] {
   253  		case "-ex":
   254  			if i+1 >= len(fields) {
   255  				return rrInit{err: &ErrMalformedRRGdbCommand{line, "-ex not followed by an argument"}}
   256  			}
   257  			arg := fields[i+1]
   258  
   259  			if !strings.HasPrefix(arg, targetCmd) {
   260  				continue
   261  			}
   262  
   263  			port = arg[len(targetCmd):]
   264  			i++
   265  
   266  		case "-l":
   267  			// skip argument
   268  			i++
   269  		}
   270  	}
   271  
   272  	if port == "" {
   273  		return rrInit{err: &ErrMalformedRRGdbCommand{line, "could not find -ex argument"}}
   274  	}
   275  
   276  	exe := fields[len(fields)-1]
   277  
   278  	return rrInit{port: port, exe: exe}
   279  }
   280  
   281  // RecordAndReplay acts like calling Record and then Replay.
   282  func RecordAndReplay(cmd []string, wd string, quiet bool, debugInfoDirs []string, redirects [3]string) (*proc.Target, string, error) {
   283  	tracedir, err := Record(cmd, wd, quiet, redirects)
   284  	if tracedir == "" {
   285  		return nil, "", err
   286  	}
   287  	t, err := Replay(tracedir, quiet, true, debugInfoDirs)
   288  	return t, tracedir, err
   289  }
   290  
   291  // safeRemoveAll removes dir and its contents but only as long as dir does
   292  // not contain directories.
   293  func safeRemoveAll(dir string) {
   294  	dh, err := os.Open(dir)
   295  	if err != nil {
   296  		return
   297  	}
   298  	defer dh.Close()
   299  	fis, err := dh.Readdir(-1)
   300  	if err != nil {
   301  		return
   302  	}
   303  	for _, fi := range fis {
   304  		if fi.IsDir() {
   305  			return
   306  		}
   307  	}
   308  	for _, fi := range fis {
   309  		if err := os.Remove(filepath.Join(dir, fi.Name())); err != nil {
   310  			return
   311  		}
   312  	}
   313  	os.Remove(dir)
   314  }