mirror of
https://github.com/beego/bee.git
synced 2024-11-30 08:21:30 +00:00
268 lines
5.3 KiB
Go
268 lines
5.3 KiB
Go
|
package gdbserial
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"unicode"
|
||
|
)
|
||
|
|
||
|
// 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) {
|
||
|
if err := checkRRAvailabe(); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
rfd, wfd, err := os.Pipe()
|
||
|
if err != nil {
|
||
|
return "", 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
|
||
|
|
||
|
done := make(chan struct{})
|
||
|
go func() {
|
||
|
bs, _ := ioutil.ReadAll(rfd)
|
||
|
tracedir = strings.TrimSpace(string(bs))
|
||
|
close(done)
|
||
|
}()
|
||
|
|
||
|
err = rrcmd.Run()
|
||
|
// ignore run errors, it could be the program crashing
|
||
|
wfd.Close()
|
||
|
<-done
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Replay starts an instance of rr in replay mode, with the specified trace
|
||
|
// directory, and connects to it.
|
||
|
func Replay(tracedir string, quiet bool) (*Process, 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
|
||
|
err = p.Dial(init.port, init.exe, 0)
|
||
|
if err != nil {
|
||
|
rrcmd.Process.Kill()
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return p, 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) (p *Process, tracedir string, err error) {
|
||
|
tracedir, err = Record(cmd, wd, quiet)
|
||
|
if tracedir == "" {
|
||
|
return nil, "", err
|
||
|
}
|
||
|
p, err = Replay(tracedir, quiet)
|
||
|
return p, tracedir, err
|
||
|
}
|