// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This program can be used as go_darwin_arm_exec by the Go tool. // It executes binaries on an iOS device using the XCode toolchain // and the ios-deploy program: https://github.com/phonegap/ios-deploy // // This script supports an extra flag, -lldb, that pauses execution // just before the main program begins and allows the user to control // the remote lldb session. This flag is appended to the end of the // script's arguments and is not passed through to the underlying // binary. // // This script requires that three environment variables be set: // GOIOS_DEV_ID: The codesigning developer id or certificate identifier // GOIOS_APP_ID: The provisioning app id prefix. Must support wildcard app ids. // GOIOS_TEAM_ID: The team id that owns the app id prefix. // $GOROOT/misc/ios contains a script, detect.go, that attempts to autodetect these. package main import ( "bytes" "errors" "flag" "fmt" "go/build" "io" "io/ioutil" "log" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "syscall" "time" ) const debug = false var errRetry = errors.New("failed to start test harness (retry attempted)") var tmpdir string var ( devID string appID string teamID string ) // lock is a file lock to serialize iOS runs. It is global to avoid the // garbage collector finalizing it, closing the file and releasing the // lock prematurely. var lock *os.File func main() { log.SetFlags(0) log.SetPrefix("go_darwin_arm_exec: ") if debug { log.Println(strings.Join(os.Args, " ")) } if len(os.Args) < 2 { log.Fatal("usage: go_darwin_arm_exec a.out") } // e.g. B393DDEB490947F5A463FD074299B6C0AXXXXXXX devID = getenv("GOIOS_DEV_ID") // e.g. Z8B3JBXXXX.org.golang.sample, Z8B3JBXXXX prefix is available at // https://developer.apple.com/membercenter/index.action#accountSummary as Team ID. appID = getenv("GOIOS_APP_ID") // e.g. Z8B3JBXXXX, available at // https://developer.apple.com/membercenter/index.action#accountSummary as Team ID. teamID = getenv("GOIOS_TEAM_ID") var err error tmpdir, err = ioutil.TempDir("", "go_darwin_arm_exec_") if err != nil { log.Fatal(err) } // This wrapper uses complicated machinery to run iOS binaries. It // works, but only when running one binary at a time. // Use a file lock to make sure only one wrapper is running at a time. // // The lock file is never deleted, to avoid concurrent locks on distinct // files with the same path. lockName := filepath.Join(os.TempDir(), "go_darwin_arm_exec.lock") lock, err = os.OpenFile(lockName, os.O_CREATE|os.O_RDONLY, 0666) if err != nil { log.Fatal(err) } if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil { log.Fatal(err) } // Approximately 1 in a 100 binaries fail to start. If it happens, // try again. These failures happen for several reasons beyond // our control, but all of them are safe to retry as they happen // before lldb encounters the initial getwd breakpoint. As we // know the tests haven't started, we are not hiding flaky tests // with this retry. for i := 0; i < 5; i++ { if i > 0 { fmt.Fprintln(os.Stderr, "start timeout, trying again") } err = run(os.Args[1], os.Args[2:]) if err == nil || err != errRetry { break } } if !debug { os.RemoveAll(tmpdir) } if err != nil { fmt.Fprintf(os.Stderr, "go_darwin_arm_exec: %v\n", err) os.Exit(1) } } func getenv(envvar string) string { s := os.Getenv(envvar) if s == "" { log.Fatalf("%s not set\nrun $GOROOT/misc/ios/detect.go to attempt to autodetect", envvar) } return s } func run(bin string, args []string) (err error) { appdir := filepath.Join(tmpdir, "gotest.app") os.RemoveAll(appdir) if err := os.MkdirAll(appdir, 0755); err != nil { return err } if err := cp(filepath.Join(appdir, "gotest"), bin); err != nil { return err } entitlementsPath := filepath.Join(tmpdir, "Entitlements.plist") if err := ioutil.WriteFile(entitlementsPath, []byte(entitlementsPlist()), 0744); err != nil { return err } if err := ioutil.WriteFile(filepath.Join(appdir, "Info.plist"), []byte(infoPlist), 0744); err != nil { return err } if err := ioutil.WriteFile(filepath.Join(appdir, "ResourceRules.plist"), []byte(resourceRules), 0744); err != nil { return err } pkgpath, err := copyLocalData(appdir) if err != nil { return err } cmd := exec.Command( "codesign", "-f", "-s", devID, "--entitlements", entitlementsPath, appdir, ) if debug { log.Println(strings.Join(cmd.Args, " ")) } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("codesign: %v", err) } oldwd, err := os.Getwd() if err != nil { return err } if err := os.Chdir(filepath.Join(appdir, "..")); err != nil { return err } defer os.Chdir(oldwd) // Setting up lldb is flaky. The test binary itself runs when // started is set to true. Everything before that is considered // part of the setup and is retried. started := false defer func() { if r := recover(); r != nil { if w, ok := r.(waitPanic); ok { err = w.err if !started { fmt.Printf("lldb setup error: %v\n", err) err = errRetry } return } panic(r) } }() 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. 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 SIGCONT --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 started = true 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), } iosdPath, err := exec.LookPath("ios-deploy") if err != nil { return nil, err } 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. "script", "-q", "-t", "0", "/dev/null", iosdPath, "--debug", "-u", "-r", "-n", `--args=`+strings.Join(args, " ")+``, "--bundle", appdir, ) if debug { log.Println(strings.Join(s.cmd.Args, " ")) } var out io.Writer = s.out if opts.lldb { out = io.MultiWriter(out, os.Stderr) } s.cmd.Stdout = out s.cmd.Stderr = out // everything of interest is on stderr s.cmd.Stdin = lldbr 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. if opts.timeout > 1*time.Second { s.timedout = make(chan struct{}) time.AfterFunc(opts.timeout-1*time.Second, func() { close(s.timedout) }) } go func() { s.exited <- s.cmd.Wait() }() 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, 10*time.Second); err != nil { panic(waitPanic{err}) } 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 := 2*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) } } } type buf struct { mu sync.Mutex buf []byte } func (w *buf) Write(in []byte) (n int, err error) { w.mu.Lock() defer w.mu.Unlock() w.buf = append(w.buf, in...) return len(in), nil } func (w *buf) LastIndex(sep []byte) int { w.mu.Lock() defer w.mu.Unlock() return bytes.LastIndex(w.buf, sep) } func (w *buf) Bytes() []byte { w.mu.Lock() defer w.mu.Unlock() 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 { timeout time.Duration lldb bool } func parseArgs(binArgs []string) (opts options, remainingArgs []string) { var flagArgs []string for _, arg := range binArgs { if strings.Contains(arg, "-test.timeout") { flagArgs = append(flagArgs, arg) } if strings.Contains(arg, "-lldb") { flagArgs = append(flagArgs, arg) continue } remainingArgs = append(remainingArgs, arg) } f := flag.NewFlagSet("", flag.ContinueOnError) f.DurationVar(&opts.timeout, "test.timeout", 0, "") f.BoolVar(&opts.lldb, "lldb", false, "") f.Parse(flagArgs) return opts, remainingArgs } func copyLocalDir(dst, src string) error { if err := os.Mkdir(dst, 0755); err != nil { return err } d, err := os.Open(src) if err != nil { return err } defer d.Close() fi, err := d.Readdir(-1) if err != nil { return err } for _, f := range fi { if f.IsDir() { if f.Name() == "testdata" { if err := cp(dst, filepath.Join(src, f.Name())); err != nil { return err } } continue } if err := cp(dst, filepath.Join(src, f.Name())); err != nil { return err } } return nil } func cp(dst, src string) error { out, err := exec.Command("cp", "-a", src, dst).CombinedOutput() if err != nil { os.Stderr.Write(out) } return err } func copyLocalData(dstbase string) (pkgpath string, err error) { cwd, err := os.Getwd() if err != nil { return "", err } finalPkgpath, underGoRoot, err := subdir() if err != nil { return "", err } cwd = strings.TrimSuffix(cwd, finalPkgpath) // Copy all immediate files and testdata directories between // the package being tested and the source root. pkgpath = "" for _, element := range strings.Split(finalPkgpath, string(filepath.Separator)) { if debug { log.Printf("copying %s", pkgpath) } pkgpath = filepath.Join(pkgpath, element) dst := filepath.Join(dstbase, pkgpath) src := filepath.Join(cwd, pkgpath) if err := copyLocalDir(dst, src); err != nil { return "", err } } // Copy timezone file. // // Typical apps have the zoneinfo.zip in the root of their app bundle, // read by the time package as the working directory at initialization. // As we move the working directory to the GOROOT pkg directory, we // install the zoneinfo.zip file in the pkgpath. if underGoRoot { err := cp( filepath.Join(dstbase, pkgpath), filepath.Join(cwd, "lib", "time", "zoneinfo.zip"), ) if err != nil { return "", err } } return finalPkgpath, nil } // subdir determines the package based on the current working directory, // and returns the path to the package source relative to $GOROOT (or $GOPATH). func subdir() (pkgpath string, underGoRoot bool, err error) { cwd, err := os.Getwd() if err != nil { return "", false, err } if root := runtime.GOROOT(); strings.HasPrefix(cwd, root) { subdir, err := filepath.Rel(root, cwd) if err != nil { return "", false, err } return subdir, true, nil } for _, p := range filepath.SplitList(build.Default.GOPATH) { if !strings.HasPrefix(cwd, p) { continue } subdir, err := filepath.Rel(p, cwd) if err == nil { return subdir, false, nil } } return "", false, fmt.Errorf( "working directory %q is not in either GOROOT(%q) or GOPATH(%q)", cwd, runtime.GOROOT(), build.Default.GOPATH, ) } const infoPlist = ` CFBundleNamegolang.gotest CFBundleSupportedPlatformsiPhoneOS CFBundleExecutablegotest CFBundleVersion1.0 CFBundleIdentifiergolang.gotest CFBundleResourceSpecificationResourceRules.plist LSRequiresIPhoneOS CFBundleDisplayNamegotest ` func entitlementsPlist() string { return ` keychain-access-groups ` + appID + `.golang.gotest get-task-allow application-identifier ` + appID + `.golang.gotest com.apple.developer.team-identifier ` + teamID + ` ` } const resourceRules = ` rules .* Info.plist omit weight 10 ResourceRules.plist omit weight 100 `