gitlab.com/Raven-IO/raven-delve@v1.22.4/pkg/proc/gdbserial/rr.go (about)

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