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