github.com/mdempsky/go@v0.0.0-20151201204031-5dd372bd1e70/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", envvar)
   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  	defer func() {
   164  		if r := recover(); r != nil {
   165  			if w, ok := r.(waitPanic); ok {
   166  				err = w.err
   167  				return
   168  			}
   169  			panic(r)
   170  		}
   171  	}()
   172  
   173  	defer exec.Command("killall", "ios-deploy").Run() // cleanup
   174  	exec.Command("killall", "ios-deploy").Run()
   175  
   176  	var opts options
   177  	opts, args = parseArgs(args)
   178  
   179  	// ios-deploy invokes lldb to give us a shell session with the app.
   180  	s, err := newSession(appdir, args, opts)
   181  	if err != nil {
   182  		return err
   183  	}
   184  	defer func() {
   185  		b := s.out.Bytes()
   186  		if err == nil && !debug {
   187  			i := bytes.Index(b, []byte("(lldb) process continue"))
   188  			if i > 0 {
   189  				b = b[i:]
   190  			}
   191  		}
   192  		os.Stdout.Write(b)
   193  	}()
   194  
   195  	// Script LLDB. Oh dear.
   196  	s.do(`process handle SIGHUP  --stop false --pass true --notify false`)
   197  	s.do(`process handle SIGPIPE --stop false --pass true --notify false`)
   198  	s.do(`process handle SIGUSR1 --stop false --pass true --notify false`)
   199  	s.do(`process handle SIGSEGV --stop false --pass true --notify false`) // does not work
   200  	s.do(`process handle SIGBUS  --stop false --pass true --notify false`) // does not work
   201  
   202  	if opts.lldb {
   203  		_, err := io.Copy(s.in, os.Stdin)
   204  		if err != io.EOF {
   205  			return err
   206  		}
   207  		return nil
   208  	}
   209  
   210  	s.do(`breakpoint set -n getwd`) // in runtime/cgo/gcc_darwin_arm.go
   211  
   212  	s.doCmd("run", "stop reason = breakpoint", 20*time.Second)
   213  
   214  	// Move the current working directory into the faux gopath.
   215  	if pkgpath != "src" {
   216  		s.do(`breakpoint delete 1`)
   217  		s.do(`expr char* $mem = (char*)malloc(512)`)
   218  		s.do(`expr $mem = (char*)getwd($mem, 512)`)
   219  		s.do(`expr $mem = (char*)strcat($mem, "/` + pkgpath + `")`)
   220  		s.do(`call (void)chdir($mem)`)
   221  	}
   222  
   223  	startTestsLen := s.out.Len()
   224  	fmt.Fprintln(s.in, `process continue`)
   225  
   226  	passed := func(out *buf) bool {
   227  		// Just to make things fun, lldb sometimes translates \n into \r\n.
   228  		return s.out.LastIndex([]byte("\nPASS\n")) > startTestsLen ||
   229  			s.out.LastIndex([]byte("\nPASS\r")) > startTestsLen ||
   230  			s.out.LastIndex([]byte("\n(lldb) PASS\n")) > startTestsLen ||
   231  			s.out.LastIndex([]byte("\n(lldb) PASS\r")) > startTestsLen
   232  	}
   233  	err = s.wait("test completion", passed, opts.timeout)
   234  	if passed(s.out) {
   235  		// The returned lldb error code is usually non-zero.
   236  		// We check for test success by scanning for the final
   237  		// PASS returned by the test harness, assuming the worst
   238  		// in its absence.
   239  		return nil
   240  	}
   241  	return err
   242  }
   243  
   244  type lldbSession struct {
   245  	cmd      *exec.Cmd
   246  	in       *os.File
   247  	out      *buf
   248  	timedout chan struct{}
   249  	exited   chan error
   250  }
   251  
   252  func newSession(appdir string, args []string, opts options) (*lldbSession, error) {
   253  	lldbr, in, err := os.Pipe()
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	s := &lldbSession{
   258  		in:     in,
   259  		out:    new(buf),
   260  		exited: make(chan error),
   261  	}
   262  
   263  	s.cmd = exec.Command(
   264  		// lldb tries to be clever with terminals.
   265  		// So we wrap it in script(1) and be clever
   266  		// right back at it.
   267  		"script",
   268  		"-q", "-t", "0",
   269  		"/dev/null",
   270  
   271  		"ios-deploy",
   272  		"--debug",
   273  		"-u",
   274  		"-r",
   275  		"-n",
   276  		`--args=`+strings.Join(args, " ")+``,
   277  		"--bundle", appdir,
   278  	)
   279  	if debug {
   280  		log.Println(strings.Join(s.cmd.Args, " "))
   281  	}
   282  
   283  	var out io.Writer = s.out
   284  	if opts.lldb {
   285  		out = io.MultiWriter(out, os.Stderr)
   286  	}
   287  	s.cmd.Stdout = out
   288  	s.cmd.Stderr = out // everything of interest is on stderr
   289  	s.cmd.Stdin = lldbr
   290  
   291  	if err := s.cmd.Start(); err != nil {
   292  		return nil, fmt.Errorf("ios-deploy failed to start: %v", err)
   293  	}
   294  
   295  	// Manage the -test.timeout here, outside of the test. There is a lot
   296  	// of moving parts in an iOS test harness (notably lldb) that can
   297  	// swallow useful stdio or cause its own ruckus.
   298  	if opts.timeout > 1*time.Second {
   299  		s.timedout = make(chan struct{})
   300  		time.AfterFunc(opts.timeout-1*time.Second, func() {
   301  			close(s.timedout)
   302  		})
   303  	}
   304  
   305  	go func() {
   306  		s.exited <- s.cmd.Wait()
   307  	}()
   308  
   309  	cond := func(out *buf) bool {
   310  		i0 := s.out.LastIndex([]byte("(lldb)"))
   311  		i1 := s.out.LastIndex([]byte("fruitstrap"))
   312  		i2 := s.out.LastIndex([]byte(" connect"))
   313  		return i0 > 0 && i1 > 0 && i2 > 0
   314  	}
   315  	if err := s.wait("lldb start", cond, 5*time.Second); err != nil {
   316  		fmt.Printf("lldb start error: %v\n", err)
   317  		return nil, errRetry
   318  	}
   319  	return s, nil
   320  }
   321  
   322  func (s *lldbSession) do(cmd string) { s.doCmd(cmd, "(lldb)", 0) }
   323  
   324  func (s *lldbSession) doCmd(cmd string, waitFor string, extraTimeout time.Duration) {
   325  	startLen := s.out.Len()
   326  	fmt.Fprintln(s.in, cmd)
   327  	cond := func(out *buf) bool {
   328  		i := s.out.LastIndex([]byte(waitFor))
   329  		return i > startLen
   330  	}
   331  	if err := s.wait(fmt.Sprintf("running cmd %q", cmd), cond, extraTimeout); err != nil {
   332  		panic(waitPanic{err})
   333  	}
   334  }
   335  
   336  func (s *lldbSession) wait(reason string, cond func(out *buf) bool, extraTimeout time.Duration) error {
   337  	doTimeout := 1*time.Second + extraTimeout
   338  	doTimedout := time.After(doTimeout)
   339  	for {
   340  		select {
   341  		case <-s.timedout:
   342  			if p := s.cmd.Process; p != nil {
   343  				p.Kill()
   344  			}
   345  			return fmt.Errorf("test timeout (%s)", reason)
   346  		case <-doTimedout:
   347  			return fmt.Errorf("command timeout (%s for %v)", reason, doTimeout)
   348  		case err := <-s.exited:
   349  			return fmt.Errorf("exited (%s: %v)", reason, err)
   350  		default:
   351  			if cond(s.out) {
   352  				return nil
   353  			}
   354  			time.Sleep(20 * time.Millisecond)
   355  		}
   356  	}
   357  }
   358  
   359  type buf struct {
   360  	mu  sync.Mutex
   361  	buf []byte
   362  }
   363  
   364  func (w *buf) Write(in []byte) (n int, err error) {
   365  	w.mu.Lock()
   366  	defer w.mu.Unlock()
   367  	w.buf = append(w.buf, in...)
   368  	return len(in), nil
   369  }
   370  
   371  func (w *buf) LastIndex(sep []byte) int {
   372  	w.mu.Lock()
   373  	defer w.mu.Unlock()
   374  	return bytes.LastIndex(w.buf, sep)
   375  }
   376  
   377  func (w *buf) Bytes() []byte {
   378  	w.mu.Lock()
   379  	defer w.mu.Unlock()
   380  
   381  	b := make([]byte, len(w.buf))
   382  	copy(b, w.buf)
   383  	return b
   384  }
   385  
   386  func (w *buf) Len() int {
   387  	w.mu.Lock()
   388  	defer w.mu.Unlock()
   389  	return len(w.buf)
   390  }
   391  
   392  type waitPanic struct {
   393  	err error
   394  }
   395  
   396  type options struct {
   397  	timeout time.Duration
   398  	lldb    bool
   399  }
   400  
   401  func parseArgs(binArgs []string) (opts options, remainingArgs []string) {
   402  	var flagArgs []string
   403  	for _, arg := range binArgs {
   404  		if strings.Contains(arg, "-test.timeout") {
   405  			flagArgs = append(flagArgs, arg)
   406  		}
   407  		if strings.Contains(arg, "-lldb") {
   408  			flagArgs = append(flagArgs, arg)
   409  			continue
   410  		}
   411  		remainingArgs = append(remainingArgs, arg)
   412  	}
   413  	f := flag.NewFlagSet("", flag.ContinueOnError)
   414  	f.DurationVar(&opts.timeout, "test.timeout", 0, "")
   415  	f.BoolVar(&opts.lldb, "lldb", false, "")
   416  	f.Parse(flagArgs)
   417  	return opts, remainingArgs
   418  
   419  }
   420  
   421  func copyLocalDir(dst, src string) error {
   422  	if err := os.Mkdir(dst, 0755); err != nil {
   423  		return err
   424  	}
   425  
   426  	d, err := os.Open(src)
   427  	if err != nil {
   428  		return err
   429  	}
   430  	defer d.Close()
   431  	fi, err := d.Readdir(-1)
   432  	if err != nil {
   433  		return err
   434  	}
   435  
   436  	for _, f := range fi {
   437  		if f.IsDir() {
   438  			if f.Name() == "testdata" {
   439  				if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
   440  					return err
   441  				}
   442  			}
   443  			continue
   444  		}
   445  		if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
   446  			return err
   447  		}
   448  	}
   449  	return nil
   450  }
   451  
   452  func cp(dst, src string) error {
   453  	out, err := exec.Command("cp", "-a", src, dst).CombinedOutput()
   454  	if err != nil {
   455  		os.Stderr.Write(out)
   456  	}
   457  	return err
   458  }
   459  
   460  func copyLocalData(dstbase string) (pkgpath string, err error) {
   461  	cwd, err := os.Getwd()
   462  	if err != nil {
   463  		return "", err
   464  	}
   465  
   466  	finalPkgpath, underGoRoot, err := subdir()
   467  	if err != nil {
   468  		return "", err
   469  	}
   470  	cwd = strings.TrimSuffix(cwd, finalPkgpath)
   471  
   472  	// Copy all immediate files and testdata directories between
   473  	// the package being tested and the source root.
   474  	pkgpath = ""
   475  	for _, element := range strings.Split(finalPkgpath, string(filepath.Separator)) {
   476  		if debug {
   477  			log.Printf("copying %s", pkgpath)
   478  		}
   479  		pkgpath = filepath.Join(pkgpath, element)
   480  		dst := filepath.Join(dstbase, pkgpath)
   481  		src := filepath.Join(cwd, pkgpath)
   482  		if err := copyLocalDir(dst, src); err != nil {
   483  			return "", err
   484  		}
   485  	}
   486  
   487  	// Copy timezone file.
   488  	//
   489  	// Typical apps have the zoneinfo.zip in the root of their app bundle,
   490  	// read by the time package as the working directory at initialization.
   491  	// As we move the working directory to the GOROOT pkg directory, we
   492  	// install the zoneinfo.zip file in the pkgpath.
   493  	if underGoRoot {
   494  		err := cp(
   495  			filepath.Join(dstbase, pkgpath),
   496  			filepath.Join(cwd, "lib", "time", "zoneinfo.zip"),
   497  		)
   498  		if err != nil {
   499  			return "", err
   500  		}
   501  	}
   502  
   503  	return finalPkgpath, nil
   504  }
   505  
   506  // subdir determines the package based on the current working directory,
   507  // and returns the path to the package source relative to $GOROOT (or $GOPATH).
   508  func subdir() (pkgpath string, underGoRoot bool, err error) {
   509  	cwd, err := os.Getwd()
   510  	if err != nil {
   511  		return "", false, err
   512  	}
   513  	if root := runtime.GOROOT(); strings.HasPrefix(cwd, root) {
   514  		subdir, err := filepath.Rel(root, cwd)
   515  		if err != nil {
   516  			return "", false, err
   517  		}
   518  		return subdir, true, nil
   519  	}
   520  
   521  	for _, p := range filepath.SplitList(build.Default.GOPATH) {
   522  		if !strings.HasPrefix(cwd, p) {
   523  			continue
   524  		}
   525  		subdir, err := filepath.Rel(p, cwd)
   526  		if err == nil {
   527  			return subdir, false, nil
   528  		}
   529  	}
   530  	return "", false, fmt.Errorf(
   531  		"working directory %q is not in either GOROOT(%q) or GOPATH(%q)",
   532  		cwd,
   533  		runtime.GOROOT(),
   534  		build.Default.GOPATH,
   535  	)
   536  }
   537  
   538  const infoPlist = `<?xml version="1.0" encoding="UTF-8"?>
   539  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   540  <plist version="1.0">
   541  <dict>
   542  <key>CFBundleName</key><string>golang.gotest</string>
   543  <key>CFBundleSupportedPlatforms</key><array><string>iPhoneOS</string></array>
   544  <key>CFBundleExecutable</key><string>gotest</string>
   545  <key>CFBundleVersion</key><string>1.0</string>
   546  <key>CFBundleIdentifier</key><string>golang.gotest</string>
   547  <key>CFBundleResourceSpecification</key><string>ResourceRules.plist</string>
   548  <key>LSRequiresIPhoneOS</key><true/>
   549  <key>CFBundleDisplayName</key><string>gotest</string>
   550  </dict>
   551  </plist>
   552  `
   553  
   554  func entitlementsPlist() string {
   555  	return `<?xml version="1.0" encoding="UTF-8"?>
   556  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   557  <plist version="1.0">
   558  <dict>
   559  	<key>keychain-access-groups</key>
   560  	<array><string>` + appID + `.golang.gotest</string></array>
   561  	<key>get-task-allow</key>
   562  	<true/>
   563  	<key>application-identifier</key>
   564  	<string>` + appID + `.golang.gotest</string>
   565  	<key>com.apple.developer.team-identifier</key>
   566  	<string>` + teamID + `</string>
   567  </dict>
   568  </plist>
   569  `
   570  }
   571  
   572  const resourceRules = `<?xml version="1.0" encoding="UTF-8"?>
   573  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   574  <plist version="1.0">
   575  <dict>
   576  	<key>rules</key>
   577  	<dict>
   578  		<key>.*</key>
   579  		<true/>
   580  		<key>Info.plist</key>
   581  		<dict>
   582  			<key>omit</key>
   583  			<true/>
   584  			<key>weight</key>
   585  			<integer>10</integer>
   586  		</dict>
   587  		<key>ResourceRules.plist</key>
   588  		<dict>
   589  			<key>omit</key>
   590  			<true/>
   591  			<key>weight</key>
   592  			<integer>100</integer>
   593  		</dict>
   594  	</dict>
   595  </dict>
   596  </plist>
   597  `