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