github.com/cnboonhan/delve@v0.0.0-20230908061759-363f2388c2fb/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/go-delve/delve/pkg/config"
    16  	"github.com/go-delve/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, stdin string, stdout proc.OutputRedirect, stderr proc.OutputRedirect) (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(stdin, stdout, stderr, 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(stdinPath string, stdoutOR proc.OutputRedirect, stderrOR proc.OutputRedirect, quiet bool) (stdin, stdout, stderr *os.File, closefn func(), err error) {
    67  	toclose := []*os.File{}
    68  
    69  	if stdinPath != "" {
    70  		stdin, err = os.Open(stdinPath)
    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(redirect proc.OutputRedirect, dflt *os.File) (f *os.File) {
    80  		if redirect.Path != "" {
    81  			f, err = os.Create(redirect.Path)
    82  			if f != nil {
    83  				toclose = append(toclose, f)
    84  			}
    85  
    86  			return f
    87  		} else if redirect.File != nil {
    88  			toclose = append(toclose, redirect.File)
    89  
    90  			return redirect.File
    91  		}
    92  
    93  		if quiet {
    94  			return nil
    95  		}
    96  
    97  		return dflt
    98  	}
    99  
   100  	stdout = create(stdoutOR, os.Stdout)
   101  	if err != nil {
   102  		return nil, nil, nil, nil, err
   103  	}
   104  
   105  	stderr = create(stderrOR, os.Stderr)
   106  	if err != nil {
   107  		return nil, nil, nil, nil, err
   108  	}
   109  
   110  	closefn = func() {
   111  		for _, f := range toclose {
   112  			_ = f.Close()
   113  		}
   114  	}
   115  
   116  	return stdin, stdout, stderr, closefn, nil
   117  }
   118  
   119  // Record uses rr to record the execution of the specified program and
   120  // returns the trace directory's path.
   121  func Record(cmd []string, wd string, quiet bool, stdin string, stdout proc.OutputRedirect, stderr proc.OutputRedirect) (tracedir string, err error) {
   122  	run, _, err := RecordAsync(cmd, wd, quiet, stdin, stdout, stderr)
   123  	if err != nil {
   124  		return "", err
   125  	}
   126  
   127  	// ignore run errors, it could be the program crashing
   128  	return run()
   129  }
   130  
   131  // Replay starts an instance of rr in replay mode, with the specified trace
   132  // directory, and connects to it.
   133  func Replay(tracedir string, quiet, deleteOnDetach bool, debugInfoDirs []string, rrOnProcessPid int, cmdline string) (*proc.TargetGroup, error) {
   134  	if err := checkRRAvailable(); err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	args := []string{
   139  		"replay",
   140  		"--dbgport=0",
   141  	}
   142  	if rrOnProcessPid != 0 {
   143  		args = append(args, fmt.Sprintf("--onprocess=%d", rrOnProcessPid))
   144  	}
   145  	args = append(args, tracedir)
   146  
   147  	rrcmd := exec.Command("rr", args...)
   148  	rrcmd.Stdout = os.Stdout
   149  	stderr, err := rrcmd.StderrPipe()
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	rrcmd.SysProcAttr = sysProcAttr(false)
   154  
   155  	initch := make(chan rrInit)
   156  	go rrStderrParser(stderr, initch, quiet)
   157  
   158  	err = rrcmd.Start()
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	init := <-initch
   164  	if init.err != nil {
   165  		rrcmd.Process.Kill()
   166  		return nil, init.err
   167  	}
   168  
   169  	p := newProcess(rrcmd.Process)
   170  	p.tracedir = tracedir
   171  	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.
   172  	if deleteOnDetach {
   173  		p.onDetach = func() {
   174  			safeRemoveAll(p.tracedir)
   175  		}
   176  	}
   177  	tgt, err := p.Dial(init.port, init.exe, cmdline, 0, debugInfoDirs, proc.StopLaunched)
   178  	if err != nil {
   179  		rrcmd.Process.Kill()
   180  		return nil, err
   181  	}
   182  
   183  	return tgt, nil
   184  }
   185  
   186  // ErrPerfEventParanoid is the error returned by Reply and Record if
   187  // /proc/sys/kernel/perf_event_paranoid is greater than 1.
   188  type ErrPerfEventParanoid struct {
   189  	actual int
   190  }
   191  
   192  func (err ErrPerfEventParanoid) Error() string {
   193  	return fmt.Sprintf("rr needs /proc/sys/kernel/perf_event_paranoid <= 1, but it is %d", err.actual)
   194  }
   195  
   196  func checkRRAvailable() error {
   197  	if _, err := exec.LookPath("rr"); err != nil {
   198  		return &ErrBackendUnavailable{}
   199  	}
   200  
   201  	// Check that /proc/sys/kernel/perf_event_paranoid doesn't exist or is <= 1.
   202  	buf, err := ioutil.ReadFile("/proc/sys/kernel/perf_event_paranoid")
   203  	if err == nil {
   204  		perfEventParanoid, _ := strconv.Atoi(strings.TrimSpace(string(buf)))
   205  		if perfEventParanoid > 1 {
   206  			return ErrPerfEventParanoid{perfEventParanoid}
   207  		}
   208  	}
   209  
   210  	return nil
   211  }
   212  
   213  type rrInit struct {
   214  	port string
   215  	exe  string
   216  	err  error
   217  }
   218  
   219  const (
   220  	rrGdbCommandPrefix = "  gdb "
   221  	rrGdbLaunchPrefix  = "Launch gdb with"
   222  	targetCmd          = "target extended-remote "
   223  )
   224  
   225  func rrStderrParser(stderr io.ReadCloser, initch chan<- rrInit, quiet bool) {
   226  	rd := bufio.NewReader(stderr)
   227  	defer stderr.Close()
   228  
   229  	for {
   230  		line, err := rd.ReadString('\n')
   231  		if err != nil {
   232  			initch <- rrInit{"", "", err}
   233  			close(initch)
   234  			return
   235  		}
   236  
   237  		if strings.HasPrefix(line, rrGdbCommandPrefix) {
   238  			initch <- rrParseGdbCommand(line[len(rrGdbCommandPrefix):])
   239  			close(initch)
   240  			break
   241  		}
   242  
   243  		if strings.HasPrefix(line, rrGdbLaunchPrefix) {
   244  			continue
   245  		}
   246  
   247  		if !quiet {
   248  			os.Stderr.Write([]byte(line))
   249  		}
   250  	}
   251  
   252  	io.Copy(os.Stderr, rd)
   253  }
   254  
   255  type ErrMalformedRRGdbCommand struct {
   256  	line, reason string
   257  }
   258  
   259  func (err *ErrMalformedRRGdbCommand) Error() string {
   260  	return fmt.Sprintf("malformed gdb command %q: %s", err.line, err.reason)
   261  }
   262  
   263  func rrParseGdbCommand(line string) rrInit {
   264  	port := ""
   265  	fields := config.SplitQuotedFields(line, '\'')
   266  	for i := 0; i < len(fields); i++ {
   267  		switch fields[i] {
   268  		case "-ex":
   269  			if i+1 >= len(fields) {
   270  				return rrInit{err: &ErrMalformedRRGdbCommand{line, "-ex not followed by an argument"}}
   271  			}
   272  			arg := fields[i+1]
   273  
   274  			if !strings.HasPrefix(arg, targetCmd) {
   275  				continue
   276  			}
   277  
   278  			port = arg[len(targetCmd):]
   279  			i++
   280  
   281  		case "-l":
   282  			// skip argument
   283  			i++
   284  		}
   285  	}
   286  
   287  	if port == "" {
   288  		return rrInit{err: &ErrMalformedRRGdbCommand{line, "could not find -ex argument"}}
   289  	}
   290  
   291  	exe := fields[len(fields)-1]
   292  
   293  	return rrInit{port: port, exe: exe}
   294  }
   295  
   296  // RecordAndReplay acts like calling Record and then Replay.
   297  func RecordAndReplay(cmd []string, wd string, quiet bool, debugInfoDirs []string, stdin string, stdout proc.OutputRedirect, stderr proc.OutputRedirect) (*proc.TargetGroup, string, error) {
   298  	tracedir, err := Record(cmd, wd, quiet, stdin, stdout, stderr)
   299  	if tracedir == "" {
   300  		return nil, "", err
   301  	}
   302  	t, err := Replay(tracedir, quiet, true, debugInfoDirs, 0, strings.Join(cmd, " "))
   303  	return t, tracedir, err
   304  }
   305  
   306  // safeRemoveAll removes dir and its contents but only as long as dir does
   307  // not contain directories.
   308  func safeRemoveAll(dir string) {
   309  	fis, err := os.ReadDir(dir)
   310  	if err != nil {
   311  		return
   312  	}
   313  	for _, fi := range fis {
   314  		if fi.IsDir() {
   315  			return
   316  		}
   317  	}
   318  	for _, fi := range fis {
   319  		if err := os.Remove(filepath.Join(dir, fi.Name())); err != nil {
   320  			return
   321  		}
   322  	}
   323  	os.Remove(dir)
   324  }