diff --git a/misc/ios/go_darwin_arm_exec.go b/misc/ios/go_darwin_arm_exec.go index debd2cdb30..3131b15675 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 { + 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 + } + 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 +} + +func (s *lldbSession) do(cmd string) { s.doCmd(cmd, "(lldb)", 0) } + +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 + } + if err := s.wait(fmt.Sprintf("running cmd %q", cmd), cond, extraTimeout); err != nil { + panic(waitPanic{err}) + } +} + +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 <-timedout: - w.printBuf() - if p := cmd.Process; p != nil { + case <-s.timedout: + if p := s.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 + 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 } - w.clearTo(i + len(str)) - return nil + time.Sleep(20 * time.Millisecond) } } - do := func(cmd string) { - fmt.Fprintln(lldb, cmd) - if err := waitFor(fmt.Sprintf("prompt after %q", cmd), "(lldb)", 0); err != nil { - panic(waitPanic{err}) - } - } - - // 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 - } - - // 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 - } - - 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 - } - - // 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") - } - 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() { - 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:] -} - -// 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 { - 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 -} - -func (w *bufWriter) isPass() bool { +func (w *buf) Bytes() []byte { 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")) + b := make([]byte, len(w.buf)) + copy(b, w.buf) + return b +} + +func (w *buf) Len() int { + w.mu.Lock() + defer w.mu.Unlock() + return len(w.buf) +} + +type waitPanic struct { + err error } type options struct {