From 3facf7ecebdf10fbd48a87aabd688082c567b78c Mon Sep 17 00:00:00 2001
From: David Crawshaw <crawshaw@golang.org>
Date: Wed, 4 Nov 2015 11:21:55 -0500
Subject: [PATCH 60/63] [release-branch.go1.5] misc/ios: keep whole buffer in
go_darwin_arm_exec
The existing go_darwin_arm_exec.go script does not work with Xcode 7,
not due to any significant changes, but just ordering and timing of
statements from lldb. Unfortunately the current design of
go_darwin_arm_exec.go makes it not obvious what gets stuck where, so
this moves from a moving buffer window to a complete buffer of the
lldb output.
The result is easier code to follow, and it works with Xcode 7.
Updates #12660.
Change-Id: I3b8b890b0bf4474119482e95d84e821a86d1eaed
Reviewed-on: https://go-review.googlesource.com/16634
Reviewed-by: Michael Matloob <matloob@golang.org>
Reviewed-on: https://go-review.googlesource.com/17146
---
misc/ios/go_darwin_arm_exec.go | 366 +++++++++++++++++------------------------
1 file changed, 149 insertions(+), 217 deletions(-)
diff --git a/misc/ios/go_darwin_arm_exec.go b/misc/ios/go_darwin_arm_exec.go
index debd2cd..3131b15 100644
--- a/misc/ios/go_darwin_arm_exec.go
+++ b/misc/ios/go_darwin_arm_exec.go
@@ -160,9 +160,6 @@ func run(bin string, args []string) (err error) {
}
defer os.Chdir(oldwd)
- type waitPanic struct {
- err error
- }
defer func() {
if r := recover(); r != nil {
if w, ok := r.(waitPanic); ok {
@@ -174,14 +171,96 @@ func run(bin string, args []string) (err error) {
}()
defer exec.Command("killall", "ios-deploy").Run() // cleanup
-
exec.Command("killall", "ios-deploy").Run()
var opts options
opts, args = parseArgs(args)
// ios-deploy invokes lldb to give us a shell session with the app.
- cmd = exec.Command(
+ s, err := newSession(appdir, args, opts)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ b := s.out.Bytes()
+ if err == nil && !debug {
+ i := bytes.Index(b, []byte("(lldb) process continue"))
+ if i > 0 {
+ b = b[i:]
+ }
+ }
+ os.Stdout.Write(b)
+ }()
+
+ // Script LLDB. Oh dear.
+ s.do(`process handle SIGHUP --stop false --pass true --notify false`)
+ s.do(`process handle SIGPIPE --stop false --pass true --notify false`)
+ s.do(`process handle SIGUSR1 --stop false --pass true --notify false`)
+ s.do(`process handle SIGSEGV --stop false --pass true --notify false`) // does not work
+ s.do(`process handle SIGBUS --stop false --pass true --notify false`) // does not work
+
+ if opts.lldb {
+ _, err := io.Copy(s.in, os.Stdin)
+ if err != io.EOF {
+ return err
+ }
+ return nil
+ }
+
+ s.do(`breakpoint set -n getwd`) // in runtime/cgo/gcc_darwin_arm.go
+
+ s.doCmd("run", "stop reason = breakpoint", 20*time.Second)
+
+ // Move the current working directory into the faux gopath.
+ if pkgpath != "src" {
+ s.do(`breakpoint delete 1`)
+ s.do(`expr char* $mem = (char*)malloc(512)`)
+ s.do(`expr $mem = (char*)getwd($mem, 512)`)
+ s.do(`expr $mem = (char*)strcat($mem, "/` + pkgpath + `")`)
+ s.do(`call (void)chdir($mem)`)
+ }
+
+ startTestsLen := s.out.Len()
+ fmt.Fprintln(s.in, `process continue`)
+
+ passed := func(out *buf) bool {
+ // Just to make things fun, lldb sometimes translates \n into \r\n.
+ return s.out.LastIndex([]byte("\nPASS\n")) > startTestsLen ||
+ s.out.LastIndex([]byte("\nPASS\r")) > startTestsLen ||
+ s.out.LastIndex([]byte("\n(lldb) PASS\n")) > startTestsLen ||
+ s.out.LastIndex([]byte("\n(lldb) PASS\r")) > startTestsLen
+ }
+ err = s.wait("test completion", passed, opts.timeout)
+ if passed(s.out) {
+ // The returned lldb error code is usually non-zero.
+ // We check for test success by scanning for the final
+ // PASS returned by the test harness, assuming the worst
+ // in its absence.
+ return nil
+ }
+ return err
+}
+
+type lldbSession struct {
+ cmd *exec.Cmd
+ in *os.File
+ out *buf
+ timedout chan struct{}
+ exited chan error
+}
+
+func newSession(appdir string, args []string, opts options) (*lldbSession, error) {
+ lldbr, in, err := os.Pipe()
+ if err != nil {
+ return nil, err
+ }
+ s := &lldbSession{
+ in: in,
+ out: new(buf),
+ exited: make(chan error),
+ }
+
+ s.cmd = exec.Command(
// lldb tries to be clever with terminals.
// So we wrap it in script(1) and be clever
// right back at it.
@@ -198,267 +277,120 @@ func run(bin string, args []string) (err error) {
"--bundle", appdir,
)
if debug {
- log.Println(strings.Join(cmd.Args, " "))
+ log.Println(strings.Join(s.cmd.Args, " "))
}
- lldbr, lldb, err := os.Pipe()
- if err != nil {
- return err
- }
- w := new(bufWriter)
+ var out io.Writer = s.out
if opts.lldb {
- mw := io.MultiWriter(w, os.Stderr)
- cmd.Stdout = mw
- cmd.Stderr = mw
- } else {
- cmd.Stdout = w
- cmd.Stderr = w // everything of interest is on stderr
+ out = io.MultiWriter(out, os.Stderr)
}
- cmd.Stdin = lldbr
+ s.cmd.Stdout = out
+ s.cmd.Stderr = out // everything of interest is on stderr
+ s.cmd.Stdin = lldbr
- if err := cmd.Start(); err != nil {
- return fmt.Errorf("ios-deploy failed to start: %v", err)
+ if err := s.cmd.Start(); err != nil {
+ return nil, fmt.Errorf("ios-deploy failed to start: %v", err)
}
// Manage the -test.timeout here, outside of the test. There is a lot
// of moving parts in an iOS test harness (notably lldb) that can
// swallow useful stdio or cause its own ruckus.
- var timedout chan struct{}
if opts.timeout > 1*time.Second {
- timedout = make(chan struct{})
+ s.timedout = make(chan struct{})
time.AfterFunc(opts.timeout-1*time.Second, func() {
- close(timedout)
+ close(s.timedout)
})
}
- exited := make(chan error)
go func() {
- exited <- cmd.Wait()
+ s.exited <- s.cmd.Wait()
}()
- waitFor := func(stage, str string, timeout time.Duration) error {
- select {
- case <-timedout:
- w.printBuf()
- if p := cmd.Process; p != nil {
- p.Kill()
- }
- return fmt.Errorf("timeout (stage %s)", stage)
- case err := <-exited:
- w.printBuf()
- return fmt.Errorf("failed (stage %s): %v", stage, err)
- case i := <-w.find(str, timeout):
- if i < 0 {
- log.Printf("timed out on stage %q, retrying", stage)
- return errRetry
- }
- w.clearTo(i + len(str))
- return nil
- }
+ cond := func(out *buf) bool {
+ i0 := s.out.LastIndex([]byte("(lldb)"))
+ i1 := s.out.LastIndex([]byte("fruitstrap"))
+ i2 := s.out.LastIndex([]byte(" connect"))
+ return i0 > 0 && i1 > 0 && i2 > 0
}
- do := func(cmd string) {
- fmt.Fprintln(lldb, cmd)
- if err := waitFor(fmt.Sprintf("prompt after %q", cmd), "(lldb)", 0); err != nil {
- panic(waitPanic{err})
- }
+ if err := s.wait("lldb start", cond, 5*time.Second); err != nil {
+ fmt.Printf("lldb start error: %v\n", err)
+ return nil, errRetry
}
+ return s, nil
+}
- // Wait for installation and connection.
- if err := waitFor("ios-deploy before run", "(lldb)", 0); err != nil {
- // Retry if we see a rare and longstanding ios-deploy bug.
- // https://github.com/phonegap/ios-deploy/issues/11
- // Assertion failed: (AMDeviceStartService(device, CFSTR("com.apple.debugserver"), &gdbfd, NULL) == 0)
- log.Printf("%v, retrying", err)
- return errRetry
- }
+func (s *lldbSession) do(cmd string) { s.doCmd(cmd, "(lldb)", 0) }
- // Script LLDB. Oh dear.
- do(`process handle SIGHUP --stop false --pass true --notify false`)
- do(`process handle SIGPIPE --stop false --pass true --notify false`)
- do(`process handle SIGUSR1 --stop false --pass true --notify false`)
- do(`process handle SIGSEGV --stop false --pass true --notify false`) // does not work
- do(`process handle SIGBUS --stop false --pass true --notify false`) // does not work
-
- if opts.lldb {
- _, err := io.Copy(lldb, os.Stdin)
- if err != io.EOF {
- return err
- }
- return nil
+func (s *lldbSession) doCmd(cmd string, waitFor string, extraTimeout time.Duration) {
+ startLen := s.out.Len()
+ fmt.Fprintln(s.in, cmd)
+ cond := func(out *buf) bool {
+ i := s.out.LastIndex([]byte(waitFor))
+ return i > startLen
}
-
- do(`breakpoint set -n getwd`) // in runtime/cgo/gcc_darwin_arm.go
-
- fmt.Fprintln(lldb, `run`)
- if err := waitFor("br getwd", "stop reason = breakpoint", 20*time.Second); err != nil {
- // At this point we see several flaky errors from the iOS
- // build infrastructure. The most common is never reaching
- // the breakpoint, which we catch with a timeout. Very
- // occasionally lldb can produce errors like:
- //
- // Breakpoint 1: no locations (pending).
- // WARNING: Unable to resolve breakpoint to any actual locations.
- //
- // As no actual test code has been executed by this point,
- // we treat all errors as recoverable.
- if err != errRetry {
- log.Printf("%v, retrying", err)
- err = errRetry
- }
- return err
- }
- if err := waitFor("br getwd prompt", "(lldb)", 0); err != nil {
- return err
+ if err := s.wait(fmt.Sprintf("running cmd %q", cmd), cond, extraTimeout); err != nil {
+ panic(waitPanic{err})
}
+}
- // Move the current working directory into the faux gopath.
- if pkgpath != "src" {
- do(`breakpoint delete 1`)
- do(`expr char* $mem = (char*)malloc(512)`)
- do(`expr $mem = (char*)getwd($mem, 512)`)
- do(`expr $mem = (char*)strcat($mem, "/` + pkgpath + `")`)
- do(`call (void)chdir($mem)`)
- }
-
- // Run the tests.
- w.trimSuffix("(lldb) ")
- fmt.Fprintln(lldb, `process continue`)
-
- // Wait for the test to complete.
- select {
- case <-timedout:
- w.printBuf()
- if p := cmd.Process; p != nil {
- p.Kill()
- }
- return errors.New("timeout running tests")
- case <-w.find("\nPASS", 0):
- passed := w.isPass()
- w.printBuf()
- if passed {
- return nil
- }
- return errors.New("test failure")
- case err := <-exited:
- // The returned lldb error code is usually non-zero.
- // We check for test success by scanning for the final
- // PASS returned by the test harness, assuming the worst
- // in its absence.
- if w.isPass() {
- err = nil
- } else if err == nil {
- err = errors.New("test failure")
+func (s *lldbSession) wait(reason string, cond func(out *buf) bool, extraTimeout time.Duration) error {
+ doTimeout := 1*time.Second + extraTimeout
+ doTimedout := time.After(doTimeout)
+ for {
+ select {
+ case <-s.timedout:
+ if p := s.cmd.Process; p != nil {
+ p.Kill()
+ }
+ return fmt.Errorf("test timeout (%s)", reason)
+ case <-doTimedout:
+ return fmt.Errorf("command timeout (%s for %v)", reason, doTimeout)
+ case err := <-s.exited:
+ return fmt.Errorf("exited (%s: %v)", reason, err)
+ default:
+ if cond(s.out) {
+ return nil
+ }
+ time.Sleep(20 * time.Millisecond)
}
- w.printBuf()
- return err
}
}
-type bufWriter struct {
- mu sync.Mutex
- buf []byte
- suffix []byte // remove from each Write
-
- findTxt []byte // search buffer on each Write
- findCh chan int // report find position
- findAfter *time.Timer
+type buf struct {
+ mu sync.Mutex
+ buf []byte
}
-func (w *bufWriter) Write(in []byte) (n int, err error) {
+func (w *buf) Write(in []byte) (n int, err error) {
w.mu.Lock()
defer w.mu.Unlock()
-
- n = len(in)
- in = bytes.TrimSuffix(in, w.suffix)
-
- if debug {
- inTxt := strings.Replace(string(in), "\n", "\\n", -1)
- findTxt := strings.Replace(string(w.findTxt), "\n", "\\n", -1)
- fmt.Printf("debug --> %s <-- debug (findTxt='%s')\n", inTxt, findTxt)
- }
-
w.buf = append(w.buf, in...)
-
- if len(w.findTxt) > 0 {
- if i := bytes.Index(w.buf, w.findTxt); i >= 0 {
- w.findCh <- i
- close(w.findCh)
- w.findTxt = nil
- w.findCh = nil
- if w.findAfter != nil {
- w.findAfter.Stop()
- w.findAfter = nil
- }
- }
- }
- return n, nil
+ return len(in), nil
}
-func (w *bufWriter) trimSuffix(p string) {
+func (w *buf) LastIndex(sep []byte) int {
w.mu.Lock()
defer w.mu.Unlock()
- w.suffix = []byte(p)
+ return bytes.LastIndex(w.buf, sep)
}
-func (w *bufWriter) printBuf() {
+func (w *buf) Bytes() []byte {
w.mu.Lock()
defer w.mu.Unlock()
- fmt.Fprintf(os.Stderr, "%s", w.buf)
- w.buf = nil
-}
-func (w *bufWriter) clearTo(i int) {
- w.mu.Lock()
- defer w.mu.Unlock()
- w.buf = w.buf[i:]
+ b := make([]byte, len(w.buf))
+ copy(b, w.buf)
+ return b
}
-// find returns a channel that will have exactly one byte index sent
-// to it when the text str appears in the buffer. If the text does not
-// appear before timeout, -1 is sent.
-//
-// A timeout of zero means no timeout.
-func (w *bufWriter) find(str string, timeout time.Duration) <-chan int {
+func (w *buf) Len() int {
w.mu.Lock()
defer w.mu.Unlock()
- if len(w.findTxt) > 0 {
- panic(fmt.Sprintf("find(%s): already trying to find %s", str, w.findTxt))
- }
- txt := []byte(str)
- ch := make(chan int, 1)
- if i := bytes.Index(w.buf, txt); i >= 0 {
- ch <- i
- close(ch)
- } else {
- w.findTxt = txt
- w.findCh = ch
- if timeout > 0 {
- w.findAfter = time.AfterFunc(timeout, func() {
- w.mu.Lock()
- defer w.mu.Unlock()
- if w.findCh == ch {
- w.findTxt = nil
- w.findCh = nil
- w.findAfter = nil
- ch <- -1
- close(ch)
- }
- })
- }
- }
- return ch
+ return len(w.buf)
}
-func (w *bufWriter) isPass() bool {
- w.mu.Lock()
- defer w.mu.Unlock()
-
- // The final stdio of lldb is non-deterministic, so we
- // scan the whole buffer.
- //
- // Just to make things fun, lldb sometimes translates \n
- // into \r\n.
- return bytes.Contains(w.buf, []byte("\nPASS\n")) || bytes.Contains(w.buf, []byte("\nPASS\r"))
+type waitPanic struct {
+ err error
}
type options struct {
--
2.6.1