github.com/freddyisaac/sicortex-golang@v0.0.0-20231019035217-e03519e66f60/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  	"syscall"
    38  	"time"
    39  )
    40  
    41  const debug = false
    42  
    43  var errRetry = errors.New("failed to start test harness (retry attempted)")
    44  
    45  var tmpdir string
    46  
    47  var (
    48  	devID  string
    49  	appID  string
    50  	teamID string
    51  )
    52  
    53  // lock is a file lock to serialize iOS runs. It is global to avoid the
    54  // garbage collector finalizing it, closing the file and releasing the
    55  // lock prematurely.
    56  var lock *os.File
    57  
    58  func main() {
    59  	log.SetFlags(0)
    60  	log.SetPrefix("go_darwin_arm_exec: ")
    61  	if debug {
    62  		log.Println(strings.Join(os.Args, " "))
    63  	}
    64  	if len(os.Args) < 2 {
    65  		log.Fatal("usage: go_darwin_arm_exec a.out")
    66  	}
    67  
    68  	// e.g. B393DDEB490947F5A463FD074299B6C0AXXXXXXX
    69  	devID = getenv("GOIOS_DEV_ID")
    70  
    71  	// e.g. Z8B3JBXXXX.org.golang.sample, Z8B3JBXXXX prefix is available at
    72  	// https://developer.apple.com/membercenter/index.action#accountSummary as Team ID.
    73  	appID = getenv("GOIOS_APP_ID")
    74  
    75  	// e.g. Z8B3JBXXXX, available at
    76  	// https://developer.apple.com/membercenter/index.action#accountSummary as Team ID.
    77  	teamID = getenv("GOIOS_TEAM_ID")
    78  
    79  	var err error
    80  	tmpdir, err = ioutil.TempDir("", "go_darwin_arm_exec_")
    81  	if err != nil {
    82  		log.Fatal(err)
    83  	}
    84  
    85  	// This wrapper uses complicated machinery to run iOS binaries. It
    86  	// works, but only when running one binary at a time.
    87  	// Use a file lock to make sure only one wrapper is running at a time.
    88  	//
    89  	// The lock file is never deleted, to avoid concurrent locks on distinct
    90  	// files with the same path.
    91  	lockName := filepath.Join(os.TempDir(), "go_darwin_arm_exec.lock")
    92  	lock, err = os.OpenFile(lockName, os.O_CREATE|os.O_RDONLY, 0666)
    93  	if err != nil {
    94  		log.Fatal(err)
    95  	}
    96  	if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil {
    97  		log.Fatal(err)
    98  	}
    99  	// Approximately 1 in a 100 binaries fail to start. If it happens,
   100  	// try again. These failures happen for several reasons beyond
   101  	// our control, but all of them are safe to retry as they happen
   102  	// before lldb encounters the initial SIGUSR2 stop. As we
   103  	// know the tests haven't started, we are not hiding flaky tests
   104  	// with this retry.
   105  	for i := 0; i < 5; i++ {
   106  		if i > 0 {
   107  			fmt.Fprintln(os.Stderr, "start timeout, trying again")
   108  		}
   109  		err = run(os.Args[1], os.Args[2:])
   110  		if err == nil || err != errRetry {
   111  			break
   112  		}
   113  	}
   114  	if !debug {
   115  		os.RemoveAll(tmpdir)
   116  	}
   117  	if err != nil {
   118  		fmt.Fprintf(os.Stderr, "go_darwin_arm_exec: %v\n", err)
   119  		os.Exit(1)
   120  	}
   121  }
   122  
   123  func getenv(envvar string) string {
   124  	s := os.Getenv(envvar)
   125  	if s == "" {
   126  		log.Fatalf("%s not set\nrun $GOROOT/misc/ios/detect.go to attempt to autodetect", envvar)
   127  	}
   128  	return s
   129  }
   130  
   131  func run(bin string, args []string) (err error) {
   132  	appdir := filepath.Join(tmpdir, "gotest.app")
   133  	os.RemoveAll(appdir)
   134  	if err := os.MkdirAll(appdir, 0755); err != nil {
   135  		return err
   136  	}
   137  
   138  	if err := cp(filepath.Join(appdir, "gotest"), bin); err != nil {
   139  		return err
   140  	}
   141  
   142  	entitlementsPath := filepath.Join(tmpdir, "Entitlements.plist")
   143  	if err := ioutil.WriteFile(entitlementsPath, []byte(entitlementsPlist()), 0744); err != nil {
   144  		return err
   145  	}
   146  	if err := ioutil.WriteFile(filepath.Join(appdir, "Info.plist"), []byte(infoPlist), 0744); err != nil {
   147  		return err
   148  	}
   149  	if err := ioutil.WriteFile(filepath.Join(appdir, "ResourceRules.plist"), []byte(resourceRules), 0744); err != nil {
   150  		return err
   151  	}
   152  
   153  	pkgpath, err := copyLocalData(appdir)
   154  	if err != nil {
   155  		return err
   156  	}
   157  
   158  	cmd := exec.Command(
   159  		"codesign",
   160  		"-f",
   161  		"-s", devID,
   162  		"--entitlements", entitlementsPath,
   163  		appdir,
   164  	)
   165  	if debug {
   166  		log.Println(strings.Join(cmd.Args, " "))
   167  	}
   168  	cmd.Stdout = os.Stdout
   169  	cmd.Stderr = os.Stderr
   170  	if err := cmd.Run(); err != nil {
   171  		return fmt.Errorf("codesign: %v", err)
   172  	}
   173  
   174  	oldwd, err := os.Getwd()
   175  	if err != nil {
   176  		return err
   177  	}
   178  	if err := os.Chdir(filepath.Join(appdir, "..")); err != nil {
   179  		return err
   180  	}
   181  	defer os.Chdir(oldwd)
   182  
   183  	// Setting up lldb is flaky. The test binary itself runs when
   184  	// started is set to true. Everything before that is considered
   185  	// part of the setup and is retried.
   186  	started := false
   187  	defer func() {
   188  		if r := recover(); r != nil {
   189  			if w, ok := r.(waitPanic); ok {
   190  				err = w.err
   191  				if !started {
   192  					fmt.Printf("lldb setup error: %v\n", err)
   193  					err = errRetry
   194  				}
   195  				return
   196  			}
   197  			panic(r)
   198  		}
   199  	}()
   200  
   201  	defer exec.Command("killall", "ios-deploy").Run() // cleanup
   202  	exec.Command("killall", "ios-deploy").Run()
   203  
   204  	var opts options
   205  	opts, args = parseArgs(args)
   206  
   207  	// Pass the suffix for the current working directory as the
   208  	// first argument to the test. For iOS, cmd/go generates
   209  	// special handling of this argument.
   210  	args = append([]string{"cwdSuffix=" + pkgpath}, args...)
   211  
   212  	// ios-deploy invokes lldb to give us a shell session with the app.
   213  	s, err := newSession(appdir, args, opts)
   214  	if err != nil {
   215  		return err
   216  	}
   217  	defer func() {
   218  		b := s.out.Bytes()
   219  		if err == nil && !debug {
   220  			i := bytes.Index(b, []byte("(lldb) process continue"))
   221  			if i > 0 {
   222  				b = b[i:]
   223  			}
   224  		}
   225  		os.Stdout.Write(b)
   226  	}()
   227  
   228  	// Script LLDB. Oh dear.
   229  	s.do(`process handle SIGHUP  --stop false --pass true --notify false`)
   230  	s.do(`process handle SIGPIPE --stop false --pass true --notify false`)
   231  	s.do(`process handle SIGUSR1 --stop false --pass true --notify false`)
   232  	s.do(`process handle SIGUSR2 --stop true --pass false --notify true`) // sent by test harness
   233  	s.do(`process handle SIGCONT --stop false --pass true --notify false`)
   234  	s.do(`process handle SIGSEGV --stop false --pass true --notify false`) // does not work
   235  	s.do(`process handle SIGBUS  --stop false --pass true --notify false`) // does not work
   236  
   237  	if opts.lldb {
   238  		_, err := io.Copy(s.in, os.Stdin)
   239  		if err != io.EOF {
   240  			return err
   241  		}
   242  		return nil
   243  	}
   244  
   245  	started = true
   246  
   247  	s.doCmd("run", "stop reason = signal SIGUSR2", 20*time.Second)
   248  
   249  	startTestsLen := s.out.Len()
   250  	fmt.Fprintln(s.in, `process continue`)
   251  
   252  	passed := func(out *buf) bool {
   253  		// Just to make things fun, lldb sometimes translates \n into \r\n.
   254  		return s.out.LastIndex([]byte("\nPASS\n")) > startTestsLen ||
   255  			s.out.LastIndex([]byte("\nPASS\r")) > startTestsLen ||
   256  			s.out.LastIndex([]byte("\n(lldb) PASS\n")) > startTestsLen ||
   257  			s.out.LastIndex([]byte("\n(lldb) PASS\r")) > startTestsLen
   258  	}
   259  	err = s.wait("test completion", passed, opts.timeout)
   260  	if passed(s.out) {
   261  		// The returned lldb error code is usually non-zero.
   262  		// We check for test success by scanning for the final
   263  		// PASS returned by the test harness, assuming the worst
   264  		// in its absence.
   265  		return nil
   266  	}
   267  	return err
   268  }
   269  
   270  type lldbSession struct {
   271  	cmd      *exec.Cmd
   272  	in       *os.File
   273  	out      *buf
   274  	timedout chan struct{}
   275  	exited   chan error
   276  }
   277  
   278  func newSession(appdir string, args []string, opts options) (*lldbSession, error) {
   279  	lldbr, in, err := os.Pipe()
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  	s := &lldbSession{
   284  		in:     in,
   285  		out:    new(buf),
   286  		exited: make(chan error),
   287  	}
   288  
   289  	iosdPath, err := exec.LookPath("ios-deploy")
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  	s.cmd = exec.Command(
   294  		// lldb tries to be clever with terminals.
   295  		// So we wrap it in script(1) and be clever
   296  		// right back at it.
   297  		"script",
   298  		"-q", "-t", "0",
   299  		"/dev/null",
   300  
   301  		iosdPath,
   302  		"--debug",
   303  		"-u",
   304  		"-r",
   305  		"-n",
   306  		`--args=`+strings.Join(args, " ")+``,
   307  		"--bundle", appdir,
   308  	)
   309  	if debug {
   310  		log.Println(strings.Join(s.cmd.Args, " "))
   311  	}
   312  
   313  	var out io.Writer = s.out
   314  	if opts.lldb {
   315  		out = io.MultiWriter(out, os.Stderr)
   316  	}
   317  	s.cmd.Stdout = out
   318  	s.cmd.Stderr = out // everything of interest is on stderr
   319  	s.cmd.Stdin = lldbr
   320  
   321  	if err := s.cmd.Start(); err != nil {
   322  		return nil, fmt.Errorf("ios-deploy failed to start: %v", err)
   323  	}
   324  
   325  	// Manage the -test.timeout here, outside of the test. There is a lot
   326  	// of moving parts in an iOS test harness (notably lldb) that can
   327  	// swallow useful stdio or cause its own ruckus.
   328  	if opts.timeout > 1*time.Second {
   329  		s.timedout = make(chan struct{})
   330  		time.AfterFunc(opts.timeout-1*time.Second, func() {
   331  			close(s.timedout)
   332  		})
   333  	}
   334  
   335  	go func() {
   336  		s.exited <- s.cmd.Wait()
   337  	}()
   338  
   339  	cond := func(out *buf) bool {
   340  		i0 := s.out.LastIndex([]byte("(lldb)"))
   341  		i1 := s.out.LastIndex([]byte("fruitstrap"))
   342  		i2 := s.out.LastIndex([]byte(" connect"))
   343  		return i0 > 0 && i1 > 0 && i2 > 0
   344  	}
   345  	if err := s.wait("lldb start", cond, 10*time.Second); err != nil {
   346  		panic(waitPanic{err})
   347  	}
   348  	return s, nil
   349  }
   350  
   351  func (s *lldbSession) do(cmd string) { s.doCmd(cmd, "(lldb)", 0) }
   352  
   353  func (s *lldbSession) doCmd(cmd string, waitFor string, extraTimeout time.Duration) {
   354  	startLen := s.out.Len()
   355  	fmt.Fprintln(s.in, cmd)
   356  	cond := func(out *buf) bool {
   357  		i := s.out.LastIndex([]byte(waitFor))
   358  		return i > startLen
   359  	}
   360  	if err := s.wait(fmt.Sprintf("running cmd %q", cmd), cond, extraTimeout); err != nil {
   361  		panic(waitPanic{err})
   362  	}
   363  }
   364  
   365  func (s *lldbSession) wait(reason string, cond func(out *buf) bool, extraTimeout time.Duration) error {
   366  	doTimeout := 2*time.Second + extraTimeout
   367  	doTimedout := time.After(doTimeout)
   368  	for {
   369  		select {
   370  		case <-s.timedout:
   371  			if p := s.cmd.Process; p != nil {
   372  				p.Kill()
   373  			}
   374  			return fmt.Errorf("test timeout (%s)", reason)
   375  		case <-doTimedout:
   376  			return fmt.Errorf("command timeout (%s for %v)", reason, doTimeout)
   377  		case err := <-s.exited:
   378  			return fmt.Errorf("exited (%s: %v)", reason, err)
   379  		default:
   380  			if cond(s.out) {
   381  				return nil
   382  			}
   383  			time.Sleep(20 * time.Millisecond)
   384  		}
   385  	}
   386  }
   387  
   388  type buf struct {
   389  	mu  sync.Mutex
   390  	buf []byte
   391  }
   392  
   393  func (w *buf) Write(in []byte) (n int, err error) {
   394  	w.mu.Lock()
   395  	defer w.mu.Unlock()
   396  	w.buf = append(w.buf, in...)
   397  	return len(in), nil
   398  }
   399  
   400  func (w *buf) LastIndex(sep []byte) int {
   401  	w.mu.Lock()
   402  	defer w.mu.Unlock()
   403  	return bytes.LastIndex(w.buf, sep)
   404  }
   405  
   406  func (w *buf) Bytes() []byte {
   407  	w.mu.Lock()
   408  	defer w.mu.Unlock()
   409  
   410  	b := make([]byte, len(w.buf))
   411  	copy(b, w.buf)
   412  	return b
   413  }
   414  
   415  func (w *buf) Len() int {
   416  	w.mu.Lock()
   417  	defer w.mu.Unlock()
   418  	return len(w.buf)
   419  }
   420  
   421  type waitPanic struct {
   422  	err error
   423  }
   424  
   425  type options struct {
   426  	timeout time.Duration
   427  	lldb    bool
   428  }
   429  
   430  func parseArgs(binArgs []string) (opts options, remainingArgs []string) {
   431  	var flagArgs []string
   432  	for _, arg := range binArgs {
   433  		if strings.Contains(arg, "-test.timeout") {
   434  			flagArgs = append(flagArgs, arg)
   435  		}
   436  		if strings.Contains(arg, "-lldb") {
   437  			flagArgs = append(flagArgs, arg)
   438  			continue
   439  		}
   440  		remainingArgs = append(remainingArgs, arg)
   441  	}
   442  	f := flag.NewFlagSet("", flag.ContinueOnError)
   443  	f.DurationVar(&opts.timeout, "test.timeout", 0, "")
   444  	f.BoolVar(&opts.lldb, "lldb", false, "")
   445  	f.Parse(flagArgs)
   446  	return opts, remainingArgs
   447  
   448  }
   449  
   450  func copyLocalDir(dst, src string) error {
   451  	if err := os.Mkdir(dst, 0755); err != nil {
   452  		return err
   453  	}
   454  
   455  	d, err := os.Open(src)
   456  	if err != nil {
   457  		return err
   458  	}
   459  	defer d.Close()
   460  	fi, err := d.Readdir(-1)
   461  	if err != nil {
   462  		return err
   463  	}
   464  
   465  	for _, f := range fi {
   466  		if f.IsDir() {
   467  			if f.Name() == "testdata" {
   468  				if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
   469  					return err
   470  				}
   471  			}
   472  			continue
   473  		}
   474  		if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
   475  			return err
   476  		}
   477  	}
   478  	return nil
   479  }
   480  
   481  func cp(dst, src string) error {
   482  	out, err := exec.Command("cp", "-a", src, dst).CombinedOutput()
   483  	if err != nil {
   484  		os.Stderr.Write(out)
   485  	}
   486  	return err
   487  }
   488  
   489  func copyLocalData(dstbase string) (pkgpath string, err error) {
   490  	cwd, err := os.Getwd()
   491  	if err != nil {
   492  		return "", err
   493  	}
   494  
   495  	finalPkgpath, underGoRoot, err := subdir()
   496  	if err != nil {
   497  		return "", err
   498  	}
   499  	cwd = strings.TrimSuffix(cwd, finalPkgpath)
   500  
   501  	// Copy all immediate files and testdata directories between
   502  	// the package being tested and the source root.
   503  	pkgpath = ""
   504  	for _, element := range strings.Split(finalPkgpath, string(filepath.Separator)) {
   505  		if debug {
   506  			log.Printf("copying %s", pkgpath)
   507  		}
   508  		pkgpath = filepath.Join(pkgpath, element)
   509  		dst := filepath.Join(dstbase, pkgpath)
   510  		src := filepath.Join(cwd, pkgpath)
   511  		if err := copyLocalDir(dst, src); err != nil {
   512  			return "", err
   513  		}
   514  	}
   515  
   516  	// Copy timezone file.
   517  	//
   518  	// Apps have the zoneinfo.zip in the root of their app bundle,
   519  	// read by the time package as the working directory at initialization.
   520  	if underGoRoot {
   521  		err := cp(
   522  			dstbase,
   523  			filepath.Join(cwd, "lib", "time", "zoneinfo.zip"),
   524  		)
   525  		if err != nil {
   526  			return "", err
   527  		}
   528  	}
   529  
   530  	return finalPkgpath, nil
   531  }
   532  
   533  // subdir determines the package based on the current working directory,
   534  // and returns the path to the package source relative to $GOROOT (or $GOPATH).
   535  func subdir() (pkgpath string, underGoRoot bool, err error) {
   536  	cwd, err := os.Getwd()
   537  	if err != nil {
   538  		return "", false, err
   539  	}
   540  	if root := runtime.GOROOT(); strings.HasPrefix(cwd, root) {
   541  		subdir, err := filepath.Rel(root, cwd)
   542  		if err != nil {
   543  			return "", false, err
   544  		}
   545  		return subdir, true, nil
   546  	}
   547  
   548  	for _, p := range filepath.SplitList(build.Default.GOPATH) {
   549  		if !strings.HasPrefix(cwd, p) {
   550  			continue
   551  		}
   552  		subdir, err := filepath.Rel(p, cwd)
   553  		if err == nil {
   554  			return subdir, false, nil
   555  		}
   556  	}
   557  	return "", false, fmt.Errorf(
   558  		"working directory %q is not in either GOROOT(%q) or GOPATH(%q)",
   559  		cwd,
   560  		runtime.GOROOT(),
   561  		build.Default.GOPATH,
   562  	)
   563  }
   564  
   565  const infoPlist = `<?xml version="1.0" encoding="UTF-8"?>
   566  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   567  <plist version="1.0">
   568  <dict>
   569  <key>CFBundleName</key><string>golang.gotest</string>
   570  <key>CFBundleSupportedPlatforms</key><array><string>iPhoneOS</string></array>
   571  <key>CFBundleExecutable</key><string>gotest</string>
   572  <key>CFBundleVersion</key><string>1.0</string>
   573  <key>CFBundleIdentifier</key><string>golang.gotest</string>
   574  <key>CFBundleResourceSpecification</key><string>ResourceRules.plist</string>
   575  <key>LSRequiresIPhoneOS</key><true/>
   576  <key>CFBundleDisplayName</key><string>gotest</string>
   577  </dict>
   578  </plist>
   579  `
   580  
   581  func entitlementsPlist() string {
   582  	return `<?xml version="1.0" encoding="UTF-8"?>
   583  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   584  <plist version="1.0">
   585  <dict>
   586  	<key>keychain-access-groups</key>
   587  	<array><string>` + appID + `.golang.gotest</string></array>
   588  	<key>get-task-allow</key>
   589  	<true/>
   590  	<key>application-identifier</key>
   591  	<string>` + appID + `.golang.gotest</string>
   592  	<key>com.apple.developer.team-identifier</key>
   593  	<string>` + teamID + `</string>
   594  </dict>
   595  </plist>
   596  `
   597  }
   598  
   599  const resourceRules = `<?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>rules</key>
   604  	<dict>
   605  		<key>.*</key>
   606  		<true/>
   607  		<key>Info.plist</key>
   608  		<dict>
   609  			<key>omit</key>
   610  			<true/>
   611  			<key>weight</key>
   612  			<integer>10</integer>
   613  		</dict>
   614  		<key>ResourceRules.plist</key>
   615  		<dict>
   616  			<key>omit</key>
   617  			<true/>
   618  			<key>weight</key>
   619  			<integer>100</integer>
   620  		</dict>
   621  	</dict>
   622  </dict>
   623  </plist>
   624  `