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 }