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