// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This program can be used as go_darwin_arm_exec by the Go tool.
// It executes binaries on an iOS device using the XCode toolchain
// and the ios-deploy program: https://github.com/phonegap/ios-deploy
package main
import (
	"bytes"
	"errors"
	"flag"
	"fmt"
	"go/build"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"
)
const debug = false
func main() {
	log.SetFlags(0)
	log.SetPrefix("go_darwin_arm_exec: ")
	if debug {
		log.Println(strings.Join(os.Args, " "))
	}
	if len(os.Args) < 2 {
		log.Fatal("usage: go_darwin_arm_exec a.out")
	}
	if err := run(os.Args[1], os.Args[2:]); err != nil {
		fmt.Fprintf(os.Stderr, "go_darwin_arm_exec: %v\n", err)
		os.Exit(1)
	}
}
func run(bin string, args []string) error {
	defer exec.Command("killall", "ios-deploy").Run() // cleanup
	exec.Command("killall", "ios-deploy").Run()
	tmpdir, err := ioutil.TempDir("", "go_darwin_arm_exec_")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tmpdir)
	appdir := filepath.Join(tmpdir, "gotest.app")
	if err := os.MkdirAll(appdir, 0755); err != nil {
		return err
	}
	if err := cp(filepath.Join(appdir, "gotest"), bin); err != nil {
		return err
	}
	entitlementsPath := filepath.Join(tmpdir, "Entitlements.plist")
	if err := ioutil.WriteFile(entitlementsPath, []byte(entitlementsPlist), 0744); err != nil {
		return err
	}
	if err := ioutil.WriteFile(filepath.Join(appdir, "Info.plist"), []byte(infoPlist), 0744); err != nil {
		return err
	}
	if err := ioutil.WriteFile(filepath.Join(appdir, "ResourceRules.plist"), []byte(resourceRules), 0744); err != nil {
		return err
	}
	pkgpath, err := copyLocalData(appdir)
	if err != nil {
		return err
	}
	cmd := exec.Command(
		"codesign",
		"-f",
		"-s", "E8BMC3FE2Z", // certificate associated with golang.org
		"--entitlements", entitlementsPath,
		appdir,
	)
	if debug {
		log.Println(strings.Join(cmd.Args, " "))
	}
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("codesign: %v", err)
	}
	if err := os.Chdir(tmpdir); err != nil {
		return err
	}
	// ios-deploy invokes lldb to give us a shell session with the app.
	cmd = exec.Command(
		// lldb tries to be clever with terminals.
		// So we wrap it in script(1) and be clever
		// right back at it.
		"script",
		"-q", "-t", "0",
		"/dev/null",
		"ios-deploy",
		"--debug",
		"-u",
		"-r",
		"-n",
		`--args=`+strings.Join(args, " ")+``,
		"--bundle", appdir,
	)
	if debug {
		log.Println(strings.Join(cmd.Args, " "))
	}
	lldbr, lldb, err := os.Pipe()
	if err != nil {
		return err
	}
	w := new(bufWriter)
	cmd.Stdout = w
	cmd.Stderr = w // everything of interest is on stderr
	cmd.Stdin = lldbr
	if err := cmd.Start(); err != nil {
		return fmt.Errorf("ios-deploy failed to start: %v", err)
	}
	// Manage the -test.timeout here, outside of the test. There is a lot
	// of moving parts in an iOS test harness (notably lldb) that can
	// swallow useful stdio or cause its own ruckus.
	var timedout chan struct{}
	if t := parseTimeout(args); t > 1*time.Second {
		timedout = make(chan struct{})
		time.AfterFunc(t-1*time.Second, func() {
			close(timedout)
		})
	}
	exited := make(chan error)
	go func() {
		exited <- cmd.Wait()
	}()
	waitFor := func(stage, str string) error {
		select {
		case <-timedout:
			w.printBuf()
			if p := cmd.Process; p != nil {
				p.Kill()
			}
			return fmt.Errorf("timeout (stage %s)", stage)
		case err := <-exited:
			w.printBuf()
			return fmt.Errorf("failed (stage %s): %v", stage, err)
		case i := <-w.find(str):
			w.clearTo(i + len(str))
			return nil
		}
	}
	do := func(cmd string) {
		fmt.Fprintln(lldb, cmd)
	}
	// Wait for installation and connection.
	if err := waitFor("ios-deploy before run", "(lldb)     connect\r\nProcess 0 connected\r\n"); err != nil {
		return err
	}
	// Script LLDB. Oh dear.
	do(`process handle SIGHUP  --stop false --pass true --notify false`)
	do(`process handle SIGPIPE --stop false --pass true --notify false`)
	do(`process handle SIGUSR1 --stop false --pass true --notify false`)
	do(`process handle SIGSEGV --stop false --pass true --notify false`) // does not work
	do(`process handle SIGBUS  --stop false --pass true --notify false`) // does not work
	if err := waitFor("handlers set", "(lldb)"); err != nil {
		return err
	}
	do(`breakpoint set -n getwd`) // in runtime/cgo/gcc_darwin_arm.go
	if err := waitFor("breakpoint set", "(lldb)"); err != nil {
		return err
	}
	do(`run`)
	if err := waitFor("br getwd", "stop reason = breakpoint"); err != nil {
		return err
	}
	if err := waitFor("br getwd prompt", "(lldb)"); err != nil {
		return err
	}
	// Move the current working directory into the faux gopath.
	do(`breakpoint delete 1`)
	do(`expr char* $mem = (char*)malloc(512)`)
	do(`expr $mem = (char*)getwd($mem, 512)`)
	do(`expr $mem = (char*)strcat($mem, "/` + pkgpath + `")`)
	do(`expr int $res = (int)chdir($mem)`)
	do(`print $res`)
	if err := waitFor("move working dir", "(int) $res = 0"); err != nil {
		return err
	}
	// Watch for SIGSEGV. Ideally lldb would never break on SIGSEGV.
	// http://golang.org/issue/10043
	go func() {
		<-w.find("stop reason = EXC_BAD_ACCESS")
		do(`bt`)
		// The backtrace has no obvious end, so we invent one.
		do(`expr int $dummy = 1`)
		do(`print $dummy`)
		<-w.find(`(int) $dummy = 1`)
		w.printBuf()
		if p := cmd.Process; p != nil {
			p.Kill()
		}
	}()
	// Run the tests.
	w.trimSuffix("(lldb) ")
	do(`process continue`)
	// Wait for the test to complete.
	select {
	case <-timedout:
		w.printBuf()
		if p := cmd.Process; p != nil {
			p.Kill()
		}
		return errors.New("timeout running tests")
	case err := <-exited:
		// The returned lldb error code is usually non-zero.
		// We check for test success by scanning for the final
		// PASS returned by the test harness, assuming the worst
		// in its absence.
		if w.isPass() {
			err = nil
		} else if err == nil {
			err = errors.New("test failure")
		}
		w.printBuf()
		return err
	}
}
type bufWriter struct {
	mu     sync.Mutex
	buf    []byte
	suffix []byte // remove from each Write
	findTxt []byte   // search buffer on each Write
	findCh  chan int // report find position
}
func (w *bufWriter) Write(in []byte) (n int, err error) {
	w.mu.Lock()
	defer w.mu.Unlock()
	n = len(in)
	in = bytes.TrimSuffix(in, w.suffix)
	w.buf = append(w.buf, in...)
	if len(w.findTxt) > 0 {
		if i := bytes.Index(w.buf, w.findTxt); i >= 0 {
			w.findCh <- i
			close(w.findCh)
			w.findTxt = nil
			w.findCh = nil
		}
	}
	return n, nil
}
func (w *bufWriter) trimSuffix(p string) {
	w.mu.Lock()
	defer w.mu.Unlock()
	w.suffix = []byte(p)
}
func (w *bufWriter) printBuf() {
	w.mu.Lock()
	defer w.mu.Unlock()
	fmt.Fprintf(os.Stderr, "%s", w.buf)
	w.buf = nil
}
func (w *bufWriter) clearTo(i int) {
	w.mu.Lock()
	defer w.mu.Unlock()
	if debug {
		fmt.Fprintf(os.Stderr, "--- go_darwin_arm_exec clear ---\n%s\n--- go_darwin_arm_exec clear ---\n", w.buf[:i])
	}
	w.buf = w.buf[i:]
}
func (w *bufWriter) find(str string) <-chan int {
	w.mu.Lock()
	defer w.mu.Unlock()
	if len(w.findTxt) > 0 {
		panic(fmt.Sprintf("find(%s): already trying to find %s", str, w.findTxt))
	}
	txt := []byte(str)
	ch := make(chan int, 1)
	if i := bytes.Index(w.buf, txt); i >= 0 {
		ch <- i
		close(ch)
	} else {
		w.findTxt = txt
		w.findCh = ch
	}
	return ch
}
func (w *bufWriter) isPass() bool {
	w.mu.Lock()
	defer w.mu.Unlock()
	// The final stdio of lldb is non-deterministic, so we
	// scan the whole buffer.
	//
	// Just to make things fun, lldb sometimes translates \n
	// into \r\n.
	return bytes.Contains(w.buf, []byte("\nPASS\n")) || bytes.Contains(w.buf, []byte("\nPASS\r"))
}
func parseTimeout(testArgs []string) (timeout time.Duration) {
	var args []string
	for _, arg := range testArgs {
		if strings.Contains(arg, "test.timeout") {
			args = append(args, arg)
		}
	}
	f := flag.NewFlagSet("", flag.ContinueOnError)
	f.DurationVar(&timeout, "test.timeout", 0, "")
	f.Parse(args)
	if debug {
		log.Printf("parseTimeout of %s, got %s", args, timeout)
	}
	return timeout
}
func copyLocalDir(dst, src string) error {
	if err := os.Mkdir(dst, 0755); err != nil {
		return err
	}
	d, err := os.Open(src)
	if err != nil {
		return err
	}
	defer d.Close()
	fi, err := d.Readdir(-1)
	if err != nil {
		return err
	}
	for _, f := range fi {
		if f.IsDir() {
			if f.Name() == "testdata" {
				if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
					return err
				}
			}
			continue
		}
		if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
			return err
		}
	}
	return nil
}
func cp(dst, src string) error {
	out, err := exec.Command("cp", "-a", src, dst).CombinedOutput()
	if err != nil {
		os.Stderr.Write(out)
	}
	return err
}
func copyLocalData(dstbase string) (pkgpath string, err error) {
	cwd, err := os.Getwd()
	if err != nil {
		return "", err
	}
	finalPkgpath, underGoRoot, err := subdir()
	if err != nil {
		return "", err
	}
	cwd = strings.TrimSuffix(cwd, finalPkgpath)
	// Copy all immediate files and testdata directories between
	// the package being tested and the source root.
	pkgpath = ""
	for _, element := range strings.Split(finalPkgpath, string(filepath.Separator)) {
		if debug {
			log.Printf("copying %s", pkgpath)
		}
		pkgpath = filepath.Join(pkgpath, element)
		dst := filepath.Join(dstbase, pkgpath)
		src := filepath.Join(cwd, pkgpath)
		if err := copyLocalDir(dst, src); err != nil {
			return "", err
		}
	}
	// Copy timezone file.
	//
	// Typical apps have the zoneinfo.zip in the root of their app bundle,
	// read by the time package as the working directory at initialization.
	// As we move the working directory to the GOROOT pkg directory, we
	// install the zoneinfo.zip file in the pkgpath.
	if underGoRoot {
		err := cp(
			filepath.Join(dstbase, pkgpath),
			filepath.Join(cwd, "lib", "time", "zoneinfo.zip"),
		)
		if err != nil {
			return "", err
		}
	}
	return finalPkgpath, nil
}
// subdir determines the package based on the current working directory,
// and returns the path to the package source relative to $GOROOT (or $GOPATH).
func subdir() (pkgpath string, underGoRoot bool, err error) {
	cwd, err := os.Getwd()
	if err != nil {
		return "", false, err
	}
	if root := runtime.GOROOT(); strings.HasPrefix(cwd, root) {
		subdir, err := filepath.Rel(root, cwd)
		if err != nil {
			return "", false, err
		}
		return subdir, true, nil
	}
	for _, p := range filepath.SplitList(build.Default.GOPATH) {
		if !strings.HasPrefix(cwd, p) {
			continue
		}
		subdir, err := filepath.Rel(p, cwd)
		if err == nil {
			return subdir, false, nil
		}
	}
	return "", false, fmt.Errorf(
		"working directory %q is not in either GOROOT(%q) or GOPATH(%q)",
		cwd,
		runtime.GOROOT(),
		build.Default.GOPATH,
	)
}
const infoPlist = `
CFBundleNamegolang.gotest
CFBundleSupportedPlatformsiPhoneOS
CFBundleExecutablegotest
CFBundleVersion1.0
CFBundleIdentifiergolang.gotest
CFBundleResourceSpecificationResourceRules.plist
LSRequiresIPhoneOS
CFBundleDisplayNamegotest
`
const devID = `YE84DJ86AZ`
const entitlementsPlist = `
	keychain-access-groups
	` + devID + `.golang.gotest
	get-task-allow
	
	application-identifier
	` + devID + `.golang.gotest
	com.apple.developer.team-identifier
	` + devID + `
`
const resourceRules = `
        rules
        
                .*
		Info.plist 
		
			omit 
			weight 10
		
		ResourceRules.plist
		
			omit 
			weight 100
		
	
`