// 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 package main import ( "bytes" "errors" "flag" "fmt" "go/build" "io/ioutil" "log" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "time" ) const debug = false 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") } if err := run(os.Args[1], os.Args[2:]); err != nil { fmt.Fprintf(os.Stderr, "go_darwin_arm_exec: %v\n", err) os.Exit(1) } } func run(bin string, args []string) error { defer exec.Command("killall", "ios-deploy").Run() // cleanup exec.Command("killall", "ios-deploy").Run() tmpdir, err := ioutil.TempDir("", "go_darwin_arm_exec_") if err != nil { log.Fatal(err) } defer os.RemoveAll(tmpdir) appdir := filepath.Join(tmpdir, "gotest.app") 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", "E8BMC3FE2Z", // certificate associated with golang.org "--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) } if err := os.Chdir(tmpdir); err != nil { return err } // ios-deploy invokes lldb to give us a shell session with the app. 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", "ios-deploy", "--debug", "-u", "-r", "-n", `--args=`+strings.Join(args, " ")+``, "--bundle", appdir, ) if debug { log.Println(strings.Join(cmd.Args, " ")) } lldbr, lldb, err := os.Pipe() if err != nil { return err } w := new(bufWriter) cmd.Stdout = w cmd.Stderr = w // everything of interest is on stderr cmd.Stdin = lldbr if err := cmd.Start(); err != nil { return 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 t := parseTimeout(args); t > 1*time.Second { timedout = make(chan struct{}) time.AfterFunc(t-1*time.Second, func() { close(timedout) }) } exited := make(chan error) go func() { exited <- cmd.Wait() }() waitFor := func(stage, str string) 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): w.clearTo(i + len(str)) return nil } } do := func(cmd string) { fmt.Fprintln(lldb, cmd) } // Wait for installation and connection. if err := waitFor("ios-deploy before run", "(lldb) connect\r\nProcess 0 connected\r\n"); err != nil { return err } // 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 err := waitFor("handlers set", "(lldb)"); err != nil { return err } do(`breakpoint set -n getwd`) // in runtime/cgo/gcc_darwin_arm.go if err := waitFor("breakpoint set", "(lldb)"); err != nil { return err } do(`run`) if err := waitFor("br getwd", "stop reason = breakpoint"); err != nil { return err } if err := waitFor("br getwd prompt", "(lldb)"); err != nil { return err } // Move the current working directory into the faux gopath. 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(`expr int $res = (int)chdir($mem)`) do(`print $res`) if err := waitFor("move working dir", "(int) $res = 0"); err != nil { return err } // Watch for SIGSEGV. Ideally lldb would never break on SIGSEGV. // http://golang.org/issue/10043 go func() { <-w.find("stop reason = EXC_BAD_ACCESS") do(`bt`) // The backtrace has no obvious end, so we invent one. do(`expr int $dummy = 1`) do(`print $dummy`) <-w.find(`(int) $dummy = 1`) w.printBuf() if p := cmd.Process; p != nil { p.Kill() } }() // Run the tests. w.trimSuffix("(lldb) ") do(`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 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 } func (w *bufWriter) Write(in []byte) (n int, err error) { w.mu.Lock() defer w.mu.Unlock() n = len(in) in = bytes.TrimSuffix(in, w.suffix) 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 } } return n, nil } func (w *bufWriter) trimSuffix(p string) { w.mu.Lock() defer w.mu.Unlock() w.suffix = []byte(p) } 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() if debug { fmt.Fprintf(os.Stderr, "--- go_darwin_arm_exec clear ---\n%s\n--- go_darwin_arm_exec clear ---\n", w.buf[:i]) } w.buf = w.buf[i:] } func (w *bufWriter) find(str string) <-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 } return ch } 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")) } func parseTimeout(testArgs []string) (timeout time.Duration) { var args []string for _, arg := range testArgs { if strings.Contains(arg, "test.timeout") { args = append(args, arg) } } f := flag.NewFlagSet("", flag.ContinueOnError) f.DurationVar(&timeout, "test.timeout", 0, "") f.Parse(args) if debug { log.Printf("parseTimeout of %s, got %s", args, timeout) } return timeout } 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 ` const devID = `YE84DJ86AZ` const entitlementsPlist = ` keychain-access-groups ` + devID + `.golang.gotest get-task-allow application-identifier ` + devID + `.golang.gotest com.apple.developer.team-identifier ` + devID + ` ` const resourceRules = ` rules .* Info.plist omit weight 10 ResourceRules.plist omit weight 100 `