delve/pkg/proc/gdbserial/rr.go
Alessandro Arzilli 8bb93e9ae1
proc/gdbserial,debugger: allow clients to stop a recording (#1890)
Allows Delve clients to stop a recording midway by sending a
Command('halt')
request.

This is implemented by changing debugger.New to start recording the
process on a separate goroutine while holding the processMutex locked.
By locking the processMutex we ensure that almost all RPC requests will
block until the recording is done, since we can not respond correctly
to any of them.
API calls that do not require manipulating or examining the target
process, such as "IsMulticlient", "SetApiVersion" and
"GetState(nowait=true)" will work while we are recording the process.

Two other internal changes are made to the API: both GetState and
Restart become asynchronous requests, like Command. Restart because
this way it can be interrupted by a StopRecording request if the
rerecord option is passed.
GetState because clients need a call that will block until the
recording is compelted and can also be interrupted with a
StopRecording.

Clients that are uninterested in allowing the user to stop a recording
can ignore this change, since eventually they will make a request to
Delve that will block until the recording is completed.

Clients that wish to support this feature must:

1. call GetState(nowait=false) after connecting to Delve, before any
   call that would need to manipulate the target process
2. allow the user to send a StopRecording request during the initial
   GetState call
3. allow the user to send a StopRecording request during any subsequent
   Restart(rerecord=true) request (if supported).

Implements #1747
2020-03-24 09:09:28 -07:00

322 lines
6.6 KiB
Go

package gdbserial
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"unicode"
"github.com/go-delve/delve/pkg/proc"
)
// RecordAsync configures rr to record the execution of the specified
// program. Returns a run function which will actually record the program, a
// stop function which will prematurely terminate the recording of the
// program.
func RecordAsync(cmd []string, wd string, quiet bool) (run func() (string, error), stop func() error, err error) {
if err := checkRRAvailabe(); err != nil {
return nil, nil, err
}
rfd, wfd, err := os.Pipe()
if err != nil {
return nil, nil, err
}
args := make([]string, 0, len(cmd)+2)
args = append(args, "record", "--print-trace-dir=3")
args = append(args, cmd...)
rrcmd := exec.Command("rr", args...)
rrcmd.Stdin = os.Stdin
if !quiet {
rrcmd.Stdout = os.Stdout
rrcmd.Stderr = os.Stderr
}
rrcmd.ExtraFiles = []*os.File{wfd}
rrcmd.Dir = wd
tracedirChan := make(chan string)
go func() {
bs, _ := ioutil.ReadAll(rfd)
tracedirChan <- strings.TrimSpace(string(bs))
}()
run = func() (string, error) {
err := rrcmd.Run()
_ = wfd.Close()
tracedir := <-tracedirChan
return tracedir, err
}
stop = func() error {
return rrcmd.Process.Signal(syscall.SIGTERM)
}
return run, stop, nil
}
// Record uses rr to record the execution of the specified program and
// returns the trace directory's path.
func Record(cmd []string, wd string, quiet bool) (tracedir string, err error) {
run, _, err := RecordAsync(cmd, wd, quiet)
if err != nil {
return "", err
}
// ignore run errors, it could be the program crashing
return run()
}
// Replay starts an instance of rr in replay mode, with the specified trace
// directory, and connects to it.
func Replay(tracedir string, quiet, deleteOnDetach bool, debugInfoDirs []string) (*proc.Target, error) {
if err := checkRRAvailabe(); err != nil {
return nil, err
}
rrcmd := exec.Command("rr", "replay", "--dbgport=0", tracedir)
rrcmd.Stdout = os.Stdout
stderr, err := rrcmd.StderrPipe()
if err != nil {
return nil, err
}
rrcmd.SysProcAttr = sysProcAttr(false)
initch := make(chan rrInit)
go rrStderrParser(stderr, initch, quiet)
err = rrcmd.Start()
if err != nil {
return nil, err
}
init := <-initch
if init.err != nil {
rrcmd.Process.Kill()
return nil, init.err
}
p := New(rrcmd.Process)
p.tracedir = tracedir
if deleteOnDetach {
p.onDetach = func() {
safeRemoveAll(p.tracedir)
}
}
tgt, err := p.Dial(init.port, init.exe, 0, debugInfoDirs, proc.StopLaunched)
if err != nil {
rrcmd.Process.Kill()
return nil, err
}
return tgt, nil
}
// ErrPerfEventParanoid is the error returned by Reply and Record if
// /proc/sys/kernel/perf_event_paranoid is greater than 1.
type ErrPerfEventParanoid struct {
actual int
}
func (err ErrPerfEventParanoid) Error() string {
return fmt.Sprintf("rr needs /proc/sys/kernel/perf_event_paranoid <= 1, but it is %d", err.actual)
}
func checkRRAvailabe() error {
if _, err := exec.LookPath("rr"); err != nil {
return &ErrBackendUnavailable{}
}
// Check that /proc/sys/kernel/perf_event_paranoid doesn't exist or is <= 1.
buf, err := ioutil.ReadFile("/proc/sys/kernel/perf_event_paranoid")
if err == nil {
perfEventParanoid, _ := strconv.Atoi(strings.TrimSpace(string(buf)))
if perfEventParanoid > 1 {
return ErrPerfEventParanoid{perfEventParanoid}
}
}
return nil
}
type rrInit struct {
port string
exe string
err error
}
const (
rrGdbCommandPrefix = " gdb "
rrGdbLaunchPrefix = "Launch gdb with"
targetCmd = "target extended-remote "
)
func rrStderrParser(stderr io.Reader, initch chan<- rrInit, quiet bool) {
rd := bufio.NewReader(stderr)
for {
line, err := rd.ReadString('\n')
if err != nil {
initch <- rrInit{"", "", err}
close(initch)
return
}
if strings.HasPrefix(line, rrGdbCommandPrefix) {
initch <- rrParseGdbCommand(line[len(rrGdbCommandPrefix):])
close(initch)
break
}
if strings.HasPrefix(line, rrGdbLaunchPrefix) {
continue
}
if !quiet {
os.Stderr.Write([]byte(line))
}
}
io.Copy(os.Stderr, rd)
}
type ErrMalformedRRGdbCommand struct {
line, reason string
}
func (err *ErrMalformedRRGdbCommand) Error() string {
return fmt.Sprintf("malformed gdb command %q: %s", err.line, err.reason)
}
func rrParseGdbCommand(line string) rrInit {
port := ""
fields := splitQuotedFields(line)
for i := 0; i < len(fields); i++ {
switch fields[i] {
case "-ex":
if i+1 >= len(fields) {
return rrInit{err: &ErrMalformedRRGdbCommand{line, "-ex not followed by an argument"}}
}
arg := fields[i+1]
if !strings.HasPrefix(arg, targetCmd) {
continue
}
port = arg[len(targetCmd):]
i++
case "-l":
// skip argument
i++
}
}
if port == "" {
return rrInit{err: &ErrMalformedRRGdbCommand{line, "could not find -ex argument"}}
}
exe := fields[len(fields)-1]
return rrInit{port: port, exe: exe}
}
// Like strings.Fields but ignores spaces inside areas surrounded
// by single quotes.
// To specify a single quote use backslash to escape it: '\''
func splitQuotedFields(in string) []string {
type stateEnum int
const (
inSpace stateEnum = iota
inField
inQuote
inQuoteEscaped
)
state := inSpace
r := []string{}
var buf bytes.Buffer
for _, ch := range in {
switch state {
case inSpace:
if ch == '\'' {
state = inQuote
} else if !unicode.IsSpace(ch) {
buf.WriteRune(ch)
state = inField
}
case inField:
if ch == '\'' {
state = inQuote
} else if unicode.IsSpace(ch) {
r = append(r, buf.String())
buf.Reset()
} else {
buf.WriteRune(ch)
}
case inQuote:
if ch == '\'' {
state = inField
} else if ch == '\\' {
state = inQuoteEscaped
} else {
buf.WriteRune(ch)
}
case inQuoteEscaped:
buf.WriteRune(ch)
state = inQuote
}
}
if buf.Len() != 0 {
r = append(r, buf.String())
}
return r
}
// RecordAndReplay acts like calling Record and then Replay.
func RecordAndReplay(cmd []string, wd string, quiet bool, debugInfoDirs []string) (*proc.Target, string, error) {
tracedir, err := Record(cmd, wd, quiet)
if tracedir == "" {
return nil, "", err
}
t, err := Replay(tracedir, quiet, true, debugInfoDirs)
return t, tracedir, err
}
// safeRemoveAll removes dir and its contents but only as long as dir does
// not contain directories.
func safeRemoveAll(dir string) {
dh, err := os.Open(dir)
if err != nil {
return
}
defer dh.Close()
fis, err := dh.Readdir(-1)
if err != nil {
return
}
for _, fi := range fis {
if fi.IsDir() {
return
}
}
for _, fi := range fis {
if err := os.Remove(filepath.Join(dir, fi.Name())); err != nil {
return
}
}
os.Remove(dir)
}