Home | History | Annotate | Download | only in ios
      1 // Copyright 2015 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 // This program can be used as go_darwin_arm_exec by the Go tool.
      6 // It executes binaries on an iOS device using the XCode toolchain
      7 // and the ios-deploy program: https://github.com/phonegap/ios-deploy
      8 //
      9 // This script supports an extra flag, -lldb, that pauses execution
     10 // just before the main program begins and allows the user to control
     11 // the remote lldb session. This flag is appended to the end of the
     12 // script's arguments and is not passed through to the underlying
     13 // binary.
     14 //
     15 // This script requires that three environment variables be set:
     16 // 	GOIOS_DEV_ID: The codesigning developer id or certificate identifier
     17 // 	GOIOS_APP_ID: The provisioning app id prefix. Must support wildcard app ids.
     18 // 	GOIOS_TEAM_ID: The team id that owns the app id prefix.
     19 // $GOROOT/misc/ios contains a script, detect.go, that attempts to autodetect these.
     20 package main
     21 
     22 import (
     23 	"bytes"
     24 	"errors"
     25 	"flag"
     26 	"fmt"
     27 	"go/build"
     28 	"io"
     29 	"io/ioutil"
     30 	"log"
     31 	"os"
     32 	"os/exec"
     33 	"path/filepath"
     34 	"runtime"
     35 	"strings"
     36 	"sync"
     37 	"time"
     38 )
     39 
     40 const debug = false
     41 
     42 var errRetry = errors.New("failed to start test harness (retry attempted)")
     43 
     44 var tmpdir string
     45 
     46 var (
     47 	devID  string
     48 	appID  string
     49 	teamID string
     50 )
     51 
     52 func main() {
     53 	log.SetFlags(0)
     54 	log.SetPrefix("go_darwin_arm_exec: ")
     55 	if debug {
     56 		log.Println(strings.Join(os.Args, " "))
     57 	}
     58 	if len(os.Args) < 2 {
     59 		log.Fatal("usage: go_darwin_arm_exec a.out")
     60 	}
     61 
     62 	// e.g. B393DDEB490947F5A463FD074299B6C0AXXXXXXX
     63 	devID = getenv("GOIOS_DEV_ID")
     64 
     65 	// e.g. Z8B3JBXXXX.org.golang.sample, Z8B3JBXXXX prefix is available at
     66 	// https://developer.apple.com/membercenter/index.action#accountSummary as Team ID.
     67 	appID = getenv("GOIOS_APP_ID")
     68 
     69 	// e.g. Z8B3JBXXXX, available at
     70 	// https://developer.apple.com/membercenter/index.action#accountSummary as Team ID.
     71 	teamID = getenv("GOIOS_TEAM_ID")
     72 
     73 	var err error
     74 	tmpdir, err = ioutil.TempDir("", "go_darwin_arm_exec_")
     75 	if err != nil {
     76 		log.Fatal(err)
     77 	}
     78 
     79 	// Approximately 1 in a 100 binaries fail to start. If it happens,
     80 	// try again. These failures happen for several reasons beyond
     81 	// our control, but all of them are safe to retry as they happen
     82 	// before lldb encounters the initial getwd breakpoint. As we
     83 	// know the tests haven't started, we are not hiding flaky tests
     84 	// with this retry.
     85 	for i := 0; i < 5; i++ {
     86 		if i > 0 {
     87 			fmt.Fprintln(os.Stderr, "start timeout, trying again")
     88 		}
     89 		err = run(os.Args[1], os.Args[2:])
     90 		if err == nil || err != errRetry {
     91 			break
     92 		}
     93 	}
     94 	if !debug {
     95 		os.RemoveAll(tmpdir)
     96 	}
     97 	if err != nil {
     98 		fmt.Fprintf(os.Stderr, "go_darwin_arm_exec: %v\n", err)
     99 		os.Exit(1)
    100 	}
    101 }
    102 
    103 func getenv(envvar string) string {
    104 	s := os.Getenv(envvar)
    105 	if s == "" {
    106 		log.Fatalf("%s not set\nrun $GOROOT/misc/ios/detect.go to attempt to autodetect", s)
    107 	}
    108 	return s
    109 }
    110 
    111 func run(bin string, args []string) (err error) {
    112 	appdir := filepath.Join(tmpdir, "gotest.app")
    113 	os.RemoveAll(appdir)
    114 	if err := os.MkdirAll(appdir, 0755); err != nil {
    115 		return err
    116 	}
    117 
    118 	if err := cp(filepath.Join(appdir, "gotest"), bin); err != nil {
    119 		return err
    120 	}
    121 
    122 	entitlementsPath := filepath.Join(tmpdir, "Entitlements.plist")
    123 	if err := ioutil.WriteFile(entitlementsPath, []byte(entitlementsPlist()), 0744); err != nil {
    124 		return err
    125 	}
    126 	if err := ioutil.WriteFile(filepath.Join(appdir, "Info.plist"), []byte(infoPlist), 0744); err != nil {
    127 		return err
    128 	}
    129 	if err := ioutil.WriteFile(filepath.Join(appdir, "ResourceRules.plist"), []byte(resourceRules), 0744); err != nil {
    130 		return err
    131 	}
    132 
    133 	pkgpath, err := copyLocalData(appdir)
    134 	if err != nil {
    135 		return err
    136 	}
    137 
    138 	cmd := exec.Command(
    139 		"codesign",
    140 		"-f",
    141 		"-s", devID,
    142 		"--entitlements", entitlementsPath,
    143 		appdir,
    144 	)
    145 	if debug {
    146 		log.Println(strings.Join(cmd.Args, " "))
    147 	}
    148 	cmd.Stdout = os.Stdout
    149 	cmd.Stderr = os.Stderr
    150 	if err := cmd.Run(); err != nil {
    151 		return fmt.Errorf("codesign: %v", err)
    152 	}
    153 
    154 	oldwd, err := os.Getwd()
    155 	if err != nil {
    156 		return err
    157 	}
    158 	if err := os.Chdir(filepath.Join(appdir, "..")); err != nil {
    159 		return err
    160 	}
    161 	defer os.Chdir(oldwd)
    162 
    163 	type waitPanic struct {
    164 		err error
    165 	}
    166 	defer func() {
    167 		if r := recover(); r != nil {
    168 			if w, ok := r.(waitPanic); ok {
    169 				err = w.err
    170 				return
    171 			}
    172 			panic(r)
    173 		}
    174 	}()
    175 
    176 	defer exec.Command("killall", "ios-deploy").Run() // cleanup
    177 
    178 	exec.Command("killall", "ios-deploy").Run()
    179 
    180 	var opts options
    181 	opts, args = parseArgs(args)
    182 
    183 	// ios-deploy invokes lldb to give us a shell session with the app.
    184 	cmd = exec.Command(
    185 		// lldb tries to be clever with terminals.
    186 		// So we wrap it in script(1) and be clever
    187 		// right back at it.
    188 		"script",
    189 		"-q", "-t", "0",
    190 		"/dev/null",
    191 
    192 		"ios-deploy",
    193 		"--debug",
    194 		"-u",
    195 		"-r",
    196 		"-n",
    197 		`--args=`+strings.Join(args, " ")+``,
    198 		"--bundle", appdir,
    199 	)
    200 	if debug {
    201 		log.Println(strings.Join(cmd.Args, " "))
    202 	}
    203 
    204 	lldbr, lldb, err := os.Pipe()
    205 	if err != nil {
    206 		return err
    207 	}
    208 	w := new(bufWriter)
    209 	if opts.lldb {
    210 		mw := io.MultiWriter(w, os.Stderr)
    211 		cmd.Stdout = mw
    212 		cmd.Stderr = mw
    213 	} else {
    214 		cmd.Stdout = w
    215 		cmd.Stderr = w // everything of interest is on stderr
    216 	}
    217 	cmd.Stdin = lldbr
    218 
    219 	if err := cmd.Start(); err != nil {
    220 		return fmt.Errorf("ios-deploy failed to start: %v", err)
    221 	}
    222 
    223 	// Manage the -test.timeout here, outside of the test. There is a lot
    224 	// of moving parts in an iOS test harness (notably lldb) that can
    225 	// swallow useful stdio or cause its own ruckus.
    226 	var timedout chan struct{}
    227 	if opts.timeout > 1*time.Second {
    228 		timedout = make(chan struct{})
    229 		time.AfterFunc(opts.timeout-1*time.Second, func() {
    230 			close(timedout)
    231 		})
    232 	}
    233 
    234 	exited := make(chan error)
    235 	go func() {
    236 		exited <- cmd.Wait()
    237 	}()
    238 
    239 	waitFor := func(stage, str string, timeout time.Duration) error {
    240 		select {
    241 		case <-timedout:
    242 			w.printBuf()
    243 			if p := cmd.Process; p != nil {
    244 				p.Kill()
    245 			}
    246 			return fmt.Errorf("timeout (stage %s)", stage)
    247 		case err := <-exited:
    248 			w.printBuf()
    249 			return fmt.Errorf("failed (stage %s): %v", stage, err)
    250 		case i := <-w.find(str, timeout):
    251 			if i < 0 {
    252 				log.Printf("timed out on stage %q, retrying", stage)
    253 				return errRetry
    254 			}
    255 			w.clearTo(i + len(str))
    256 			return nil
    257 		}
    258 	}
    259 	do := func(cmd string) {
    260 		fmt.Fprintln(lldb, cmd)
    261 		if err := waitFor(fmt.Sprintf("prompt after %q", cmd), "(lldb)", 0); err != nil {
    262 			panic(waitPanic{err})
    263 		}
    264 	}
    265 
    266 	// Wait for installation and connection.
    267 	if err := waitFor("ios-deploy before run", "(lldb)", 0); err != nil {
    268 		// Retry if we see a rare and longstanding ios-deploy bug.
    269 		// https://github.com/phonegap/ios-deploy/issues/11
    270 		//	Assertion failed: (AMDeviceStartService(device, CFSTR("com.apple.debugserver"), &gdbfd, NULL) == 0)
    271 		log.Printf("%v, retrying", err)
    272 		return errRetry
    273 	}
    274 
    275 	// Script LLDB. Oh dear.
    276 	do(`process handle SIGHUP  --stop false --pass true --notify false`)
    277 	do(`process handle SIGPIPE --stop false --pass true --notify false`)
    278 	do(`process handle SIGUSR1 --stop false --pass true --notify false`)
    279 	do(`process handle SIGSEGV --stop false --pass true --notify false`) // does not work
    280 	do(`process handle SIGBUS  --stop false --pass true --notify false`) // does not work
    281 
    282 	if opts.lldb {
    283 		_, err := io.Copy(lldb, os.Stdin)
    284 		if err != io.EOF {
    285 			return err
    286 		}
    287 		return nil
    288 	}
    289 
    290 	do(`breakpoint set -n getwd`) // in runtime/cgo/gcc_darwin_arm.go
    291 
    292 	fmt.Fprintln(lldb, `run`)
    293 	if err := waitFor("br getwd", "stop reason = breakpoint", 20*time.Second); err != nil {
    294 		// At this point we see several flaky errors from the iOS
    295 		// build infrastructure. The most common is never reaching
    296 		// the breakpoint, which we catch with a timeout. Very
    297 		// occasionally lldb can produce errors like:
    298 		//
    299 		//	Breakpoint 1: no locations (pending).
    300 		//	WARNING:  Unable to resolve breakpoint to any actual locations.
    301 		//
    302 		// As no actual test code has been executed by this point,
    303 		// we treat all errors as recoverable.
    304 		if err != errRetry {
    305 			log.Printf("%v, retrying", err)
    306 			err = errRetry
    307 		}
    308 		return err
    309 	}
    310 	if err := waitFor("br getwd prompt", "(lldb)", 0); err != nil {
    311 		return err
    312 	}
    313 
    314 	// Move the current working directory into the faux gopath.
    315 	if pkgpath != "src" {
    316 		do(`breakpoint delete 1`)
    317 		do(`expr char* $mem = (char*)malloc(512)`)
    318 		do(`expr $mem = (char*)getwd($mem, 512)`)
    319 		do(`expr $mem = (char*)strcat($mem, "/` + pkgpath + `")`)
    320 		do(`call (void)chdir($mem)`)
    321 	}
    322 
    323 	// Run the tests.
    324 	w.trimSuffix("(lldb) ")
    325 	fmt.Fprintln(lldb, `process continue`)
    326 
    327 	// Wait for the test to complete.
    328 	select {
    329 	case <-timedout:
    330 		w.printBuf()
    331 		if p := cmd.Process; p != nil {
    332 			p.Kill()
    333 		}
    334 		return errors.New("timeout running tests")
    335 	case <-w.find("\nPASS", 0):
    336 		passed := w.isPass()
    337 		w.printBuf()
    338 		if passed {
    339 			return nil
    340 		}
    341 		return errors.New("test failure")
    342 	case err := <-exited:
    343 		// The returned lldb error code is usually non-zero.
    344 		// We check for test success by scanning for the final
    345 		// PASS returned by the test harness, assuming the worst
    346 		// in its absence.
    347 		if w.isPass() {
    348 			err = nil
    349 		} else if err == nil {
    350 			err = errors.New("test failure")
    351 		}
    352 		w.printBuf()
    353 		return err
    354 	}
    355 }
    356 
    357 type bufWriter struct {
    358 	mu     sync.Mutex
    359 	buf    []byte
    360 	suffix []byte // remove from each Write
    361 
    362 	findTxt   []byte   // search buffer on each Write
    363 	findCh    chan int // report find position
    364 	findAfter *time.Timer
    365 }
    366 
    367 func (w *bufWriter) Write(in []byte) (n int, err error) {
    368 	w.mu.Lock()
    369 	defer w.mu.Unlock()
    370 
    371 	n = len(in)
    372 	in = bytes.TrimSuffix(in, w.suffix)
    373 
    374 	if debug {
    375 		inTxt := strings.Replace(string(in), "\n", "\\n", -1)
    376 		findTxt := strings.Replace(string(w.findTxt), "\n", "\\n", -1)
    377 		fmt.Printf("debug --> %s <-- debug (findTxt='%s')\n", inTxt, findTxt)
    378 	}
    379 
    380 	w.buf = append(w.buf, in...)
    381 
    382 	if len(w.findTxt) > 0 {
    383 		if i := bytes.Index(w.buf, w.findTxt); i >= 0 {
    384 			w.findCh <- i
    385 			close(w.findCh)
    386 			w.findTxt = nil
    387 			w.findCh = nil
    388 			if w.findAfter != nil {
    389 				w.findAfter.Stop()
    390 				w.findAfter = nil
    391 			}
    392 		}
    393 	}
    394 	return n, nil
    395 }
    396 
    397 func (w *bufWriter) trimSuffix(p string) {
    398 	w.mu.Lock()
    399 	defer w.mu.Unlock()
    400 	w.suffix = []byte(p)
    401 }
    402 
    403 func (w *bufWriter) printBuf() {
    404 	w.mu.Lock()
    405 	defer w.mu.Unlock()
    406 	fmt.Fprintf(os.Stderr, "%s", w.buf)
    407 	w.buf = nil
    408 }
    409 
    410 func (w *bufWriter) clearTo(i int) {
    411 	w.mu.Lock()
    412 	defer w.mu.Unlock()
    413 	w.buf = w.buf[i:]
    414 }
    415 
    416 // find returns a channel that will have exactly one byte index sent
    417 // to it when the text str appears in the buffer. If the text does not
    418 // appear before timeout, -1 is sent.
    419 //
    420 // A timeout of zero means no timeout.
    421 func (w *bufWriter) find(str string, timeout time.Duration) <-chan int {
    422 	w.mu.Lock()
    423 	defer w.mu.Unlock()
    424 	if len(w.findTxt) > 0 {
    425 		panic(fmt.Sprintf("find(%s): already trying to find %s", str, w.findTxt))
    426 	}
    427 	txt := []byte(str)
    428 	ch := make(chan int, 1)
    429 	if i := bytes.Index(w.buf, txt); i >= 0 {
    430 		ch <- i
    431 		close(ch)
    432 	} else {
    433 		w.findTxt = txt
    434 		w.findCh = ch
    435 		if timeout > 0 {
    436 			w.findAfter = time.AfterFunc(timeout, func() {
    437 				w.mu.Lock()
    438 				defer w.mu.Unlock()
    439 				if w.findCh == ch {
    440 					w.findTxt = nil
    441 					w.findCh = nil
    442 					w.findAfter = nil
    443 					ch <- -1
    444 					close(ch)
    445 				}
    446 			})
    447 		}
    448 	}
    449 	return ch
    450 }
    451 
    452 func (w *bufWriter) isPass() bool {
    453 	w.mu.Lock()
    454 	defer w.mu.Unlock()
    455 
    456 	// The final stdio of lldb is non-deterministic, so we
    457 	// scan the whole buffer.
    458 	//
    459 	// Just to make things fun, lldb sometimes translates \n
    460 	// into \r\n.
    461 	return bytes.Contains(w.buf, []byte("\nPASS\n")) || bytes.Contains(w.buf, []byte("\nPASS\r"))
    462 }
    463 
    464 type options struct {
    465 	timeout time.Duration
    466 	lldb    bool
    467 }
    468 
    469 func parseArgs(binArgs []string) (opts options, remainingArgs []string) {
    470 	var flagArgs []string
    471 	for _, arg := range binArgs {
    472 		if strings.Contains(arg, "-test.timeout") {
    473 			flagArgs = append(flagArgs, arg)
    474 		}
    475 		if strings.Contains(arg, "-lldb") {
    476 			flagArgs = append(flagArgs, arg)
    477 			continue
    478 		}
    479 		remainingArgs = append(remainingArgs, arg)
    480 	}
    481 	f := flag.NewFlagSet("", flag.ContinueOnError)
    482 	f.DurationVar(&opts.timeout, "test.timeout", 0, "")
    483 	f.BoolVar(&opts.lldb, "lldb", false, "")
    484 	f.Parse(flagArgs)
    485 	return opts, remainingArgs
    486 
    487 }
    488 
    489 func copyLocalDir(dst, src string) error {
    490 	if err := os.Mkdir(dst, 0755); err != nil {
    491 		return err
    492 	}
    493 
    494 	d, err := os.Open(src)
    495 	if err != nil {
    496 		return err
    497 	}
    498 	defer d.Close()
    499 	fi, err := d.Readdir(-1)
    500 	if err != nil {
    501 		return err
    502 	}
    503 
    504 	for _, f := range fi {
    505 		if f.IsDir() {
    506 			if f.Name() == "testdata" {
    507 				if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
    508 					return err
    509 				}
    510 			}
    511 			continue
    512 		}
    513 		if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
    514 			return err
    515 		}
    516 	}
    517 	return nil
    518 }
    519 
    520 func cp(dst, src string) error {
    521 	out, err := exec.Command("cp", "-a", src, dst).CombinedOutput()
    522 	if err != nil {
    523 		os.Stderr.Write(out)
    524 	}
    525 	return err
    526 }
    527 
    528 func copyLocalData(dstbase string) (pkgpath string, err error) {
    529 	cwd, err := os.Getwd()
    530 	if err != nil {
    531 		return "", err
    532 	}
    533 
    534 	finalPkgpath, underGoRoot, err := subdir()
    535 	if err != nil {
    536 		return "", err
    537 	}
    538 	cwd = strings.TrimSuffix(cwd, finalPkgpath)
    539 
    540 	// Copy all immediate files and testdata directories between
    541 	// the package being tested and the source root.
    542 	pkgpath = ""
    543 	for _, element := range strings.Split(finalPkgpath, string(filepath.Separator)) {
    544 		if debug {
    545 			log.Printf("copying %s", pkgpath)
    546 		}
    547 		pkgpath = filepath.Join(pkgpath, element)
    548 		dst := filepath.Join(dstbase, pkgpath)
    549 		src := filepath.Join(cwd, pkgpath)
    550 		if err := copyLocalDir(dst, src); err != nil {
    551 			return "", err
    552 		}
    553 	}
    554 
    555 	// Copy timezone file.
    556 	//
    557 	// Typical apps have the zoneinfo.zip in the root of their app bundle,
    558 	// read by the time package as the working directory at initialization.
    559 	// As we move the working directory to the GOROOT pkg directory, we
    560 	// install the zoneinfo.zip file in the pkgpath.
    561 	if underGoRoot {
    562 		err := cp(
    563 			filepath.Join(dstbase, pkgpath),
    564 			filepath.Join(cwd, "lib", "time", "zoneinfo.zip"),
    565 		)
    566 		if err != nil {
    567 			return "", err
    568 		}
    569 	}
    570 
    571 	return finalPkgpath, nil
    572 }
    573 
    574 // subdir determines the package based on the current working directory,
    575 // and returns the path to the package source relative to $GOROOT (or $GOPATH).
    576 func subdir() (pkgpath string, underGoRoot bool, err error) {
    577 	cwd, err := os.Getwd()
    578 	if err != nil {
    579 		return "", false, err
    580 	}
    581 	if root := runtime.GOROOT(); strings.HasPrefix(cwd, root) {
    582 		subdir, err := filepath.Rel(root, cwd)
    583 		if err != nil {
    584 			return "", false, err
    585 		}
    586 		return subdir, true, nil
    587 	}
    588 
    589 	for _, p := range filepath.SplitList(build.Default.GOPATH) {
    590 		if !strings.HasPrefix(cwd, p) {
    591 			continue
    592 		}
    593 		subdir, err := filepath.Rel(p, cwd)
    594 		if err == nil {
    595 			return subdir, false, nil
    596 		}
    597 	}
    598 	return "", false, fmt.Errorf(
    599 		"working directory %q is not in either GOROOT(%q) or GOPATH(%q)",
    600 		cwd,
    601 		runtime.GOROOT(),
    602 		build.Default.GOPATH,
    603 	)
    604 }
    605 
    606 const infoPlist = `<?xml version="1.0" encoding="UTF-8"?>
    607 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    608 <plist version="1.0">
    609 <dict>
    610 <key>CFBundleName</key><string>golang.gotest</string>
    611 <key>CFBundleSupportedPlatforms</key><array><string>iPhoneOS</string></array>
    612 <key>CFBundleExecutable</key><string>gotest</string>
    613 <key>CFBundleVersion</key><string>1.0</string>
    614 <key>CFBundleIdentifier</key><string>golang.gotest</string>
    615 <key>CFBundleResourceSpecification</key><string>ResourceRules.plist</string>
    616 <key>LSRequiresIPhoneOS</key><true/>
    617 <key>CFBundleDisplayName</key><string>gotest</string>
    618 </dict>
    619 </plist>
    620 `
    621 
    622 func entitlementsPlist() string {
    623 	return `<?xml version="1.0" encoding="UTF-8"?>
    624 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    625 <plist version="1.0">
    626 <dict>
    627 	<key>keychain-access-groups</key>
    628 	<array><string>` + appID + `.golang.gotest</string></array>
    629 	<key>get-task-allow</key>
    630 	<true/>
    631 	<key>application-identifier</key>
    632 	<string>` + appID + `.golang.gotest</string>
    633 	<key>com.apple.developer.team-identifier</key>
    634 	<string>` + teamID + `</string>
    635 </dict>
    636 </plist>
    637 `
    638 }
    639 
    640 const resourceRules = `<?xml version="1.0" encoding="UTF-8"?>
    641 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    642 <plist version="1.0">
    643 <dict>
    644 	<key>rules</key>
    645 	<dict>
    646 		<key>.*</key>
    647 		<true/>
    648 		<key>Info.plist</key>
    649 		<dict>
    650 			<key>omit</key>
    651 			<true/>
    652 			<key>weight</key>
    653 			<integer>10</integer>
    654 		</dict>
    655 		<key>ResourceRules.plist</key>
    656 		<dict>
    657 			<key>omit</key>
    658 			<true/>
    659 			<key>weight</key>
    660 			<integer>100</integer>
    661 		</dict>
    662 	</dict>
    663 </dict>
    664 </plist>
    665 `
    666