github.com/sbinet/go@v0.0.0-20160827155028-54d7de7dd62b/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 getwd breakpoint. 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 // ios-deploy invokes lldb to give us a shell session with the app. 208 s, err := newSession(appdir, args, opts) 209 if err != nil { 210 return err 211 } 212 defer func() { 213 b := s.out.Bytes() 214 if err == nil && !debug { 215 i := bytes.Index(b, []byte("(lldb) process continue")) 216 if i > 0 { 217 b = b[i:] 218 } 219 } 220 os.Stdout.Write(b) 221 }() 222 223 // Script LLDB. Oh dear. 224 s.do(`process handle SIGHUP --stop false --pass true --notify false`) 225 s.do(`process handle SIGPIPE --stop false --pass true --notify false`) 226 s.do(`process handle SIGUSR1 --stop false --pass true --notify false`) 227 s.do(`process handle SIGCONT --stop false --pass true --notify false`) 228 s.do(`process handle SIGSEGV --stop false --pass true --notify false`) // does not work 229 s.do(`process handle SIGBUS --stop false --pass true --notify false`) // does not work 230 231 if opts.lldb { 232 _, err := io.Copy(s.in, os.Stdin) 233 if err != io.EOF { 234 return err 235 } 236 return nil 237 } 238 239 s.do(`breakpoint set -n getwd`) // in runtime/cgo/gcc_darwin_arm.go 240 241 started = true 242 243 s.doCmd("run", "stop reason = breakpoint", 20*time.Second) 244 245 // Move the current working directory into the faux gopath. 246 if pkgpath != "src" { 247 s.do(`breakpoint delete 1`) 248 s.do(`expr char* $mem = (char*)malloc(512)`) 249 s.do(`expr $mem = (char*)getwd($mem, 512)`) 250 s.do(`expr $mem = (char*)strcat($mem, "/` + pkgpath + `")`) 251 s.do(`call (void)chdir($mem)`) 252 } 253 254 startTestsLen := s.out.Len() 255 fmt.Fprintln(s.in, `process continue`) 256 257 passed := func(out *buf) bool { 258 // Just to make things fun, lldb sometimes translates \n into \r\n. 259 return s.out.LastIndex([]byte("\nPASS\n")) > startTestsLen || 260 s.out.LastIndex([]byte("\nPASS\r")) > startTestsLen || 261 s.out.LastIndex([]byte("\n(lldb) PASS\n")) > startTestsLen || 262 s.out.LastIndex([]byte("\n(lldb) PASS\r")) > startTestsLen 263 } 264 err = s.wait("test completion", passed, opts.timeout) 265 if passed(s.out) { 266 // The returned lldb error code is usually non-zero. 267 // We check for test success by scanning for the final 268 // PASS returned by the test harness, assuming the worst 269 // in its absence. 270 return nil 271 } 272 return err 273 } 274 275 type lldbSession struct { 276 cmd *exec.Cmd 277 in *os.File 278 out *buf 279 timedout chan struct{} 280 exited chan error 281 } 282 283 func newSession(appdir string, args []string, opts options) (*lldbSession, error) { 284 lldbr, in, err := os.Pipe() 285 if err != nil { 286 return nil, err 287 } 288 s := &lldbSession{ 289 in: in, 290 out: new(buf), 291 exited: make(chan error), 292 } 293 294 iosdPath, err := exec.LookPath("ios-deploy") 295 if err != nil { 296 return nil, err 297 } 298 s.cmd = exec.Command( 299 // lldb tries to be clever with terminals. 300 // So we wrap it in script(1) and be clever 301 // right back at it. 302 "script", 303 "-q", "-t", "0", 304 "/dev/null", 305 306 iosdPath, 307 "--debug", 308 "-u", 309 "-r", 310 "-n", 311 `--args=`+strings.Join(args, " ")+``, 312 "--bundle", appdir, 313 ) 314 if debug { 315 log.Println(strings.Join(s.cmd.Args, " ")) 316 } 317 318 var out io.Writer = s.out 319 if opts.lldb { 320 out = io.MultiWriter(out, os.Stderr) 321 } 322 s.cmd.Stdout = out 323 s.cmd.Stderr = out // everything of interest is on stderr 324 s.cmd.Stdin = lldbr 325 326 if err := s.cmd.Start(); err != nil { 327 return nil, fmt.Errorf("ios-deploy failed to start: %v", err) 328 } 329 330 // Manage the -test.timeout here, outside of the test. There is a lot 331 // of moving parts in an iOS test harness (notably lldb) that can 332 // swallow useful stdio or cause its own ruckus. 333 if opts.timeout > 1*time.Second { 334 s.timedout = make(chan struct{}) 335 time.AfterFunc(opts.timeout-1*time.Second, func() { 336 close(s.timedout) 337 }) 338 } 339 340 go func() { 341 s.exited <- s.cmd.Wait() 342 }() 343 344 cond := func(out *buf) bool { 345 i0 := s.out.LastIndex([]byte("(lldb)")) 346 i1 := s.out.LastIndex([]byte("fruitstrap")) 347 i2 := s.out.LastIndex([]byte(" connect")) 348 return i0 > 0 && i1 > 0 && i2 > 0 349 } 350 if err := s.wait("lldb start", cond, 10*time.Second); err != nil { 351 panic(waitPanic{err}) 352 } 353 return s, nil 354 } 355 356 func (s *lldbSession) do(cmd string) { s.doCmd(cmd, "(lldb)", 0) } 357 358 func (s *lldbSession) doCmd(cmd string, waitFor string, extraTimeout time.Duration) { 359 startLen := s.out.Len() 360 fmt.Fprintln(s.in, cmd) 361 cond := func(out *buf) bool { 362 i := s.out.LastIndex([]byte(waitFor)) 363 return i > startLen 364 } 365 if err := s.wait(fmt.Sprintf("running cmd %q", cmd), cond, extraTimeout); err != nil { 366 panic(waitPanic{err}) 367 } 368 } 369 370 func (s *lldbSession) wait(reason string, cond func(out *buf) bool, extraTimeout time.Duration) error { 371 doTimeout := 2*time.Second + extraTimeout 372 doTimedout := time.After(doTimeout) 373 for { 374 select { 375 case <-s.timedout: 376 if p := s.cmd.Process; p != nil { 377 p.Kill() 378 } 379 return fmt.Errorf("test timeout (%s)", reason) 380 case <-doTimedout: 381 return fmt.Errorf("command timeout (%s for %v)", reason, doTimeout) 382 case err := <-s.exited: 383 return fmt.Errorf("exited (%s: %v)", reason, err) 384 default: 385 if cond(s.out) { 386 return nil 387 } 388 time.Sleep(20 * time.Millisecond) 389 } 390 } 391 } 392 393 type buf struct { 394 mu sync.Mutex 395 buf []byte 396 } 397 398 func (w *buf) Write(in []byte) (n int, err error) { 399 w.mu.Lock() 400 defer w.mu.Unlock() 401 w.buf = append(w.buf, in...) 402 return len(in), nil 403 } 404 405 func (w *buf) LastIndex(sep []byte) int { 406 w.mu.Lock() 407 defer w.mu.Unlock() 408 return bytes.LastIndex(w.buf, sep) 409 } 410 411 func (w *buf) Bytes() []byte { 412 w.mu.Lock() 413 defer w.mu.Unlock() 414 415 b := make([]byte, len(w.buf)) 416 copy(b, w.buf) 417 return b 418 } 419 420 func (w *buf) Len() int { 421 w.mu.Lock() 422 defer w.mu.Unlock() 423 return len(w.buf) 424 } 425 426 type waitPanic struct { 427 err error 428 } 429 430 type options struct { 431 timeout time.Duration 432 lldb bool 433 } 434 435 func parseArgs(binArgs []string) (opts options, remainingArgs []string) { 436 var flagArgs []string 437 for _, arg := range binArgs { 438 if strings.Contains(arg, "-test.timeout") { 439 flagArgs = append(flagArgs, arg) 440 } 441 if strings.Contains(arg, "-lldb") { 442 flagArgs = append(flagArgs, arg) 443 continue 444 } 445 remainingArgs = append(remainingArgs, arg) 446 } 447 f := flag.NewFlagSet("", flag.ContinueOnError) 448 f.DurationVar(&opts.timeout, "test.timeout", 0, "") 449 f.BoolVar(&opts.lldb, "lldb", false, "") 450 f.Parse(flagArgs) 451 return opts, remainingArgs 452 453 } 454 455 func copyLocalDir(dst, src string) error { 456 if err := os.Mkdir(dst, 0755); err != nil { 457 return err 458 } 459 460 d, err := os.Open(src) 461 if err != nil { 462 return err 463 } 464 defer d.Close() 465 fi, err := d.Readdir(-1) 466 if err != nil { 467 return err 468 } 469 470 for _, f := range fi { 471 if f.IsDir() { 472 if f.Name() == "testdata" { 473 if err := cp(dst, filepath.Join(src, f.Name())); err != nil { 474 return err 475 } 476 } 477 continue 478 } 479 if err := cp(dst, filepath.Join(src, f.Name())); err != nil { 480 return err 481 } 482 } 483 return nil 484 } 485 486 func cp(dst, src string) error { 487 out, err := exec.Command("cp", "-a", src, dst).CombinedOutput() 488 if err != nil { 489 os.Stderr.Write(out) 490 } 491 return err 492 } 493 494 func copyLocalData(dstbase string) (pkgpath string, err error) { 495 cwd, err := os.Getwd() 496 if err != nil { 497 return "", err 498 } 499 500 finalPkgpath, underGoRoot, err := subdir() 501 if err != nil { 502 return "", err 503 } 504 cwd = strings.TrimSuffix(cwd, finalPkgpath) 505 506 // Copy all immediate files and testdata directories between 507 // the package being tested and the source root. 508 pkgpath = "" 509 for _, element := range strings.Split(finalPkgpath, string(filepath.Separator)) { 510 if debug { 511 log.Printf("copying %s", pkgpath) 512 } 513 pkgpath = filepath.Join(pkgpath, element) 514 dst := filepath.Join(dstbase, pkgpath) 515 src := filepath.Join(cwd, pkgpath) 516 if err := copyLocalDir(dst, src); err != nil { 517 return "", err 518 } 519 } 520 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 if underGoRoot { 528 err := cp( 529 filepath.Join(dstbase, pkgpath), 530 filepath.Join(cwd, "lib", "time", "zoneinfo.zip"), 531 ) 532 if err != nil { 533 return "", err 534 } 535 } 536 537 return finalPkgpath, nil 538 } 539 540 // subdir determines the package based on the current working directory, 541 // and returns the path to the package source relative to $GOROOT (or $GOPATH). 542 func subdir() (pkgpath string, underGoRoot bool, err error) { 543 cwd, err := os.Getwd() 544 if err != nil { 545 return "", false, err 546 } 547 if root := runtime.GOROOT(); strings.HasPrefix(cwd, root) { 548 subdir, err := filepath.Rel(root, cwd) 549 if err != nil { 550 return "", false, err 551 } 552 return subdir, true, nil 553 } 554 555 for _, p := range filepath.SplitList(build.Default.GOPATH) { 556 if !strings.HasPrefix(cwd, p) { 557 continue 558 } 559 subdir, err := filepath.Rel(p, cwd) 560 if err == nil { 561 return subdir, false, nil 562 } 563 } 564 return "", false, fmt.Errorf( 565 "working directory %q is not in either GOROOT(%q) or GOPATH(%q)", 566 cwd, 567 runtime.GOROOT(), 568 build.Default.GOPATH, 569 ) 570 } 571 572 const infoPlist = `<?xml version="1.0" encoding="UTF-8"?> 573 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 574 <plist version="1.0"> 575 <dict> 576 <key>CFBundleName</key><string>golang.gotest</string> 577 <key>CFBundleSupportedPlatforms</key><array><string>iPhoneOS</string></array> 578 <key>CFBundleExecutable</key><string>gotest</string> 579 <key>CFBundleVersion</key><string>1.0</string> 580 <key>CFBundleIdentifier</key><string>golang.gotest</string> 581 <key>CFBundleResourceSpecification</key><string>ResourceRules.plist</string> 582 <key>LSRequiresIPhoneOS</key><true/> 583 <key>CFBundleDisplayName</key><string>gotest</string> 584 </dict> 585 </plist> 586 ` 587 588 func entitlementsPlist() string { 589 return `<?xml version="1.0" encoding="UTF-8"?> 590 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 591 <plist version="1.0"> 592 <dict> 593 <key>keychain-access-groups</key> 594 <array><string>` + appID + `.golang.gotest</string></array> 595 <key>get-task-allow</key> 596 <true/> 597 <key>application-identifier</key> 598 <string>` + appID + `.golang.gotest</string> 599 <key>com.apple.developer.team-identifier</key> 600 <string>` + teamID + `</string> 601 </dict> 602 </plist> 603 ` 604 } 605 606 const resourceRules = `<?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>rules</key> 611 <dict> 612 <key>.*</key> 613 <true/> 614 <key>Info.plist</key> 615 <dict> 616 <key>omit</key> 617 <true/> 618 <key>weight</key> 619 <integer>10</integer> 620 </dict> 621 <key>ResourceRules.plist</key> 622 <dict> 623 <key>omit</key> 624 <true/> 625 <key>weight</key> 626 <integer>100</integer> 627 </dict> 628 </dict> 629 </dict> 630 </plist> 631 `