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