]> Cypherpunks repositories - gostls13.git/commitdiff
misc/ios: keep whole buffer in go_darwin_arm_exec
authorDavid Crawshaw <crawshaw@golang.org>
Wed, 4 Nov 2015 16:21:55 +0000 (11:21 -0500)
committerDavid Crawshaw <crawshaw@golang.org>
Wed, 4 Nov 2015 20:00:35 +0000 (20:00 +0000)
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>
misc/ios/go_darwin_arm_exec.go

index 4d658e770c325aa2bf6b9c940c40f58769ed51b7..828efe91c11f77fa9f4f867c913794a4d2446ad3 100644 (file)
@@ -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 {