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 `