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 `