github.com/huandu/go@v0.0.0-20151114150818-04e615e41150/misc/ios/go_darwin_arm_exec.go (about)

     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  `