Skip to content

Commit

Permalink
Merge branch 'main' into dm-fix-runwda-ios16
Browse files Browse the repository at this point in the history
  • Loading branch information
dmissmann authored Nov 15, 2024
2 parents 8237296 + a8240fa commit c832f3d
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 147 deletions.
29 changes: 9 additions & 20 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,19 @@
GO_IOS_BINARY_NAME=ios
NCM_BINARY_NAME=go-ncm

# Define only if compiling for system different than our own
OS=
ARCH=

# Detect the system architecture
UNAME_S := $(shell uname -s)
UNAME_M := $(shell uname -m)

# Default GOARCH value
GOARCH := amd64

# Set GOARCH based on the detected architecture
ifeq ($(UNAME_M),x86_64)
GOARCH := amd64
else ifeq ($(UNAME_M),armv7l)
GOARCH := arm
else ifeq ($(UNAME_M),aarch64)
GOARCH := arm64
# Add more architecture mappings as needed
endif
# Prepend each non-empty OS/ARCH definition to "go" command
GOEXEC=$(strip $(foreach v,OS ARCH,$(and $($v),GO$v=$($v) )) go)

# Build the Go program
build:
@go work use .
@GOARCH=$(GOARCH) go build -o $(GO_IOS_BINARY_NAME) ./main.go
@go work use ./ncm
@CGO_ENABLED=1 GOARCH=$(GOARCH) go build -o $(NCM_BINARY_NAME) ./cmd/cdc-ncm/main.go
@$(GOEXEC) work use .
@$(GOEXEC) build -o $(GO_IOS_BINARY_NAME) ./main.go
@$(GOEXEC) work use ./ncm
@CGO_ENABLED=1 $(GOEXEC) build -o $(NCM_BINARY_NAME) ./cmd/cdc-ncm/main.go

# Run the Go program with sudo
run: build
Expand Down
15 changes: 14 additions & 1 deletion ios/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ func ConnectTUNDevice(remoteIp string, port int, d DeviceEntry) (*net.TCPConn, e
return connectTUN(remoteIp, port)
}

addr, _ := net.ResolveTCPAddr("tcp4", fmt.Sprintf("localhost:%d", d.UserspaceTUNPort))
addr, _ := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", d.UserspaceTUNHost, d.UserspaceTUNPort))
conn, err := net.DialTCP("tcp", nil, addr)
if err != nil {
return nil, fmt.Errorf("ConnectUserSpaceTunnel: failed to dial: %w", err)
Expand Down Expand Up @@ -328,6 +328,9 @@ func connectTUN(address string, port int) (*net.TCPConn, error) {
// 60-105 is leetspeek for go-ios :-D
const defaultHttpApiPort = 60105

// defaultHttpApiHost is the host on which the HTTP-Server runs, by default it is 127.0.0.1
const defaultHttpApiHost = "127.0.0.1"

// DefaultHttpApiPort is the port on which we start the HTTP-Server for exposing started tunnels
// if GO_IOS_AGENT_PORT is set, we use that port. Otherwise we use the default port 60106.
// 60-105 is leetspeek for go-ios :-D
Expand All @@ -338,3 +341,13 @@ func HttpApiPort() int {
}
return port
}

// DefaultHttpApiHost is the host on which the HTTP-Server runs, by default it is 127.0.0.1
// if GO_IOS_AGENT_HOST is set, we use that host. Otherwise we use the default host
func HttpApiHost() string {
host := os.Getenv("GO_IOS_AGENT_HOST")
if host == "" {
return defaultHttpApiHost
}
return host
}
20 changes: 16 additions & 4 deletions ios/instruments/processcontrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ func (p *ProcessControl) LaunchApp(bundleID string, my_opts map[string]any) (uin
}
maps.Copy(opts, my_opts)
// Xcode sends all these, no idea if we need them for sth. later.
//"CA_ASSERT_MAIN_THREAD_TRANSACTIONS": "0", "CA_DEBUG_TRANSACTIONS": "0", "LLVM_PROFILE_FILE": "/dev/null", "METAL_DEBUG_ERROR_MODE": "0", "METAL_DEVICE_WRAPPER_TYPE": "1",
//"OS_ACTIVITY_DT_MODE": "YES", "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", "__XPC_LLVM_PROFILE_FILE": "/dev/null"
// "CA_ASSERT_MAIN_THREAD_TRANSACTIONS": "0", "CA_DEBUG_TRANSACTIONS": "0", "LLVM_PROFILE_FILE": "/dev/null", "METAL_DEBUG_ERROR_MODE": "0", "METAL_DEVICE_WRAPPER_TYPE": "1",
// "OS_ACTIVITY_DT_MODE": "YES", "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", "__XPC_LLVM_PROFILE_FILE": "/dev/null"
// NSUnbufferedIO seems to make the app send its logs via instruments using the outputReceived:fromProcess:atTime: selector
// We'll supply per default to get logs
env := map[string]interface{}{"NSUnbufferedIO": "YES"}
Expand All @@ -40,8 +40,8 @@ func (p *ProcessControl) LaunchAppWithArgs(bundleID string, my_args []interface{
}
maps.Copy(opts, my_opts)
// Xcode sends all these, no idea if we need them for sth. later.
//"CA_ASSERT_MAIN_THREAD_TRANSACTIONS": "0", "CA_DEBUG_TRANSACTIONS": "0", "LLVM_PROFILE_FILE": "/dev/null", "METAL_DEBUG_ERROR_MODE": "0", "METAL_DEVICE_WRAPPER_TYPE": "1",
//"OS_ACTIVITY_DT_MODE": "YES", "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", "__XPC_LLVM_PROFILE_FILE": "/dev/null"
// "CA_ASSERT_MAIN_THREAD_TRANSACTIONS": "0", "CA_DEBUG_TRANSACTIONS": "0", "LLVM_PROFILE_FILE": "/dev/null", "METAL_DEBUG_ERROR_MODE": "0", "METAL_DEVICE_WRAPPER_TYPE": "1",
// "OS_ACTIVITY_DT_MODE": "YES", "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", "__XPC_LLVM_PROFILE_FILE": "/dev/null"
// NSUnbufferedIO seems to make the app send its logs via instruments using the outputReceived:fromProcess:atTime: selector
// We'll supply per default to get logs
env := map[string]interface{}{"NSUnbufferedIO": "YES"}
Expand All @@ -62,6 +62,18 @@ func NewProcessControl(device ios.DeviceEntry) (*ProcessControl, error) {
return &ProcessControl{processControlChannel: processControlChannel, conn: dtxConn}, nil
}

// DisableMemoryLimit disables the memory limit of a process.
func (p ProcessControl) DisableMemoryLimit(pid uint64) (bool, error) {
msg, err := p.processControlChannel.MethodCall("requestDisableMemoryLimitsForPid:", pid)
if err != nil {
return false, err
}
if disabled, ok := msg.Payload[0].(bool); ok {
return disabled, nil
}
return false, fmt.Errorf("expected int 0 or 1 as payload of msg: %v", msg)
}

// KillProcess kills the process on the device.
func (p ProcessControl) KillProcess(pid uint64) error {
_, err := p.processControlChannel.MethodCall("killPid:", pid)
Expand Down
1 change: 1 addition & 0 deletions ios/listdevices.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type DeviceEntry struct {
Address string
Rsd RsdPortProvider
UserspaceTUN bool
UserspaceTUNHost string
UserspaceTUNPort int
}

Expand Down
6 changes: 5 additions & 1 deletion ios/nskeyedarchiver/objectivec_classes.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,11 @@ func NewNSError(object map[string]interface{}, objects []interface{}) interface{
}

func (err NSError) Error() string {
return fmt.Sprintf("Error code: %d, Domain: %s, User info: %v", err.ErrorCode, err.Domain, err.UserInfo)
var description any = "no description available"
if d, ok := err.UserInfo["NSLocalizedDescription"]; ok {
description = d
}
return fmt.Sprintf("%v (Error code: %d, Domain: %s)", description, err.ErrorCode, err.Domain)
}

// Apples Reference Date is Jan 1st 2001 00:00
Expand Down
129 changes: 55 additions & 74 deletions ios/testmanagerd/xcuitestrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,95 +221,75 @@ const (

const testBundleSuffix = "UITests.xctrunner"

func RunXCUITest(bundleID string, testRunnerBundleID string, xctestConfigName string, device ios.DeviceEntry, env map[string]interface{}, testsToRun []string, testsToSkip []string, testListener *TestListener, isXCTest bool) ([]TestSuite, error) {
// FIXME: this is redundant code, getting the app list twice and creating the appinfos twice
// just to generate the xctestConfigFileName. Should be cleaned up at some point.
installationProxy, err := installationproxy.New(device)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("RunXCUITest: cannot connect to installation proxy: %w", err)
}
defer installationProxy.Close()

if testRunnerBundleID == "" {
testRunnerBundleID = bundleID + testBundleSuffix
}

apps, err := installationProxy.BrowseUserApps()
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("RunXCUITest: cannot browse user apps: %w", err)
}

if bundleID != "" && xctestConfigName == "" {
info, err := getappInfo(bundleID, apps)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("RunXCUITest: cannot get app information: %w", err)
}

xctestConfigName = info.bundleName + "UITests.xctest"
}

return RunXCUIWithBundleIdsCtx(context.TODO(), bundleID, testRunnerBundleID, xctestConfigName, device, nil, env, testsToRun, testsToSkip, testListener, isXCTest)
// TestConfig specifies the parameters of a test execution
type TestConfig struct {
// The identifier of the app under test
BundleId string
// The identifier of the test runner. For unit tests (non-UI tests) this is also the
// app under test (BundleId can be left empty) as the .xctest bundle is packaged into the app under test
TestRunnerBundleId string
// XctestConfigName is the name of the
XctestConfigName string
// Env is passed as environment variables to the test runner
Env map[string]any
// Args are passed to the test runner as launch arguments
Args []string
// TestsToRun specifies a list of tests that should be executed. All other tests are ignored. To execute all tests
// pass nil.
// The format of the values is {PRODUCT_MODULE_NAME}.{CLASS}/{METHOD} where {PRODUCT_MODULE_NAME} and {METHOD} are
// optional. If {METHOD} is omitted, all tests of {CLASS} are executed
TestsToRun []string
// TestsToSkip specifies a list of tests that should be skipped. See TestsToRun for the format
TestsToSkip []string
// XcTest needs to be set to true if the TestRunnerBundleId is a unit test and not a UI test
XcTest bool
// The device on which the test is executed
Device ios.DeviceEntry
// The listener for receiving results
Listener *TestListener
}

func RunXCUIWithBundleIdsCtx(
ctx context.Context,
bundleID string,
testRunnerBundleID string,
xctestConfigFileName string,
device ios.DeviceEntry,
args []string,
env map[string]interface{},
testsToRun []string,
testsToSkip []string,
testListener *TestListener,
isXCTest bool,
) ([]TestSuite, error) {
version, err := ios.GetProductVersion(device)
func RunTestWithConfig(ctx context.Context, testConfig TestConfig) ([]TestSuite, error) {
if len(testConfig.TestRunnerBundleId) == 0 {
return nil, fmt.Errorf("RunTestWithConfig: testConfig.TestRunnerBundleId can not be empty")
}
version, err := ios.GetProductVersion(testConfig.Device)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("RunXCUIWithBundleIdsCtx: cannot determine iOS version: %w", err)
}

if version.LessThan(ios.IOS14()) {
log.Debugf("iOS version: %s detected, running with ios11 support", version)
return runXCUIWithBundleIdsXcode11Ctx(ctx, bundleID, testRunnerBundleID, xctestConfigFileName, device, args, env, testsToRun, testsToSkip, testListener, isXCTest, version)
return runXCUIWithBundleIdsXcode11Ctx(ctx, testConfig, version)
}

if version.LessThan(ios.IOS17()) {
log.Debugf("iOS version: %s detected, running with ios14 support", version)
return runXUITestWithBundleIdsXcode12Ctx(ctx, bundleID, testRunnerBundleID, xctestConfigFileName, device, args, env, testsToRun, testsToSkip, testListener, isXCTest, version)
return runXUITestWithBundleIdsXcode12Ctx(ctx, testConfig, version)
}

log.Debugf("iOS version: %s detected, running with ios17 support", version)
return runXUITestWithBundleIdsXcode15Ctx(ctx, bundleID, testRunnerBundleID, xctestConfigFileName, device, args, env, testsToRun, testsToSkip, testListener, isXCTest, version)
return runXUITestWithBundleIdsXcode15Ctx(ctx, testConfig, version)
}

func runXUITestWithBundleIdsXcode15Ctx(
ctx context.Context,
bundleID string,
testRunnerBundleID string,
xctestConfigFileName string,
device ios.DeviceEntry,
args []string,
env map[string]interface{},
testsToRun []string,
testsToSkip []string,
testListener *TestListener,
isXCTest bool,
config TestConfig,
version *semver.Version,
) ([]TestSuite, error) {
conn1, err := dtx.NewTunnelConnection(device, testmanagerdiOS17)
conn1, err := dtx.NewTunnelConnection(config.Device, testmanagerdiOS17)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot create a tunnel connection to testmanagerd: %w", err)
}
defer conn1.Close()

conn2, err := dtx.NewTunnelConnection(device, testmanagerdiOS17)
conn2, err := dtx.NewTunnelConnection(config.Device, testmanagerdiOS17)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot create a tunnel connection to testmanagerd: %w", err)
}
defer conn2.Close()

installationProxy, err := installationproxy.New(device)
installationProxy, err := installationproxy.New(config.Device)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot connect to installation proxy: %w", err)
}
Expand All @@ -319,7 +299,7 @@ func runXUITestWithBundleIdsXcode15Ctx(
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot browse user apps: %w", err)
}

testAppInfo, err := getappInfo(testRunnerBundleID, apps)
testAppInfo, err := getappInfo(config.TestRunnerBundleId, apps)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot get test app information: %w", err)
}
Expand All @@ -328,8 +308,8 @@ func runXUITestWithBundleIdsXcode15Ctx(
testApp: testAppInfo,
}

if bundleID != "" {
appInfo, err := getappInfo(bundleID, apps)
if config.BundleId != "" {
appInfo, err := getappInfo(config.BundleId, apps)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot get app information: %w", err)
}
Expand All @@ -338,8 +318,8 @@ func runXUITestWithBundleIdsXcode15Ctx(
}

testSessionID := uuid.New()
testconfig := createTestConfig(info, testSessionID, xctestConfigFileName, testsToRun, testsToSkip, isXCTest, version)
ideDaemonProxy1 := newDtxProxyWithConfig(conn1, testconfig, testListener)
testconfig := createTestConfig(info, testSessionID, config.XctestConfigName, config.TestsToRun, config.TestsToSkip, config.XcTest, version)
ideDaemonProxy1 := newDtxProxyWithConfig(conn1, testconfig, config.Listener)

localCaps := nskeyedarchiver.XCTCapabilities{CapabilitiesDictionary: map[string]interface{}{
"XCTIssue capability": uint64(1),
Expand All @@ -359,26 +339,26 @@ func runXUITestWithBundleIdsXcode15Ctx(
}
log.WithField("receivedCaps", receivedCaps).Info("got capabilities")

appserviceConn, err := appservice.New(device)
appserviceConn, err := appservice.New(config.Device)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot connect to app service: %w", err)
}
defer appserviceConn.Close()

testRunnerLaunch, err := startTestRunner17(device, appserviceConn, "", testRunnerBundleID, strings.ToUpper(testSessionID.String()), info.testApp.path+"/PlugIns/"+xctestConfigFileName, args, env, isXCTest)
testRunnerLaunch, err := startTestRunner17(appserviceConn, config.TestRunnerBundleId, strings.ToUpper(testSessionID.String()), info.testApp.path+"/PlugIns/"+config.XctestConfigName, config.Args, config.Env, config.XcTest)
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot start test runner: %w", err)
}

defer testRunnerLaunch.Close()
go func() {
_, err := io.Copy(testListener.logWriter, testRunnerLaunch)
_, err := io.Copy(config.Listener.logWriter, testRunnerLaunch)
if err != nil {
log.Warn("copying stdout failed", log.WithError(err))
}
}()

ideDaemonProxy2 := newDtxProxyWithConfig(conn2, testconfig, testListener)
ideDaemonProxy2 := newDtxProxyWithConfig(conn2, testconfig, config.Listener)
caps, err := ideDaemonProxy2.daemonConnection.initiateControlSessionWithCapabilities(nskeyedarchiver.XCTCapabilities{CapabilitiesDictionary: map[string]interface{}{}})
if err != nil {
return make([]TestSuite, 0), fmt.Errorf("runXUITestWithBundleIdsXcode15Ctx: cannot initiate a control session with capabilities: %w", err)
Expand All @@ -404,16 +384,16 @@ func runXUITestWithBundleIdsXcode15Ctx(
if !errors.Is(conn1.Err(), dtx.ErrConnectionClosed) {
log.WithError(conn1.Err()).Error("conn1 closed unexpectedly")
}
testListener.FinishWithError(errors.New("lost connection to testmanagerd. the test-runner may have been killed"))
config.Listener.FinishWithError(errors.New("lost connection to testmanagerd. the test-runner may have been killed"))
break
case <-conn2.Closed():
log.Debug("conn2 closed")
if !errors.Is(conn2.Err(), dtx.ErrConnectionClosed) {
log.WithError(conn2.Err()).Error("conn2 closed unexpectedly")
}
testListener.FinishWithError(errors.New("lost connection to testmanagerd. the test-runner may have been killed"))
config.Listener.FinishWithError(errors.New("lost connection to testmanagerd. the test-runner may have been killed"))
break
case <-testListener.Done():
case <-config.Listener.Done():
break
case <-ctx.Done():
break
Expand All @@ -428,7 +408,7 @@ func runXUITestWithBundleIdsXcode15Ctx(

log.Debugf("Done running test")

return testListener.TestSuites, testListener.err
return config.Listener.TestSuites, config.Listener.err
}

type processKiller interface {
Expand All @@ -446,7 +426,7 @@ func killTestRunner(killer processKiller, pid int) error {
return nil
}

func startTestRunner17(device ios.DeviceEntry, appserviceConn *appservice.Connection, xctestConfigPath string, bundleID string, sessionIdentifier string, testBundlePath string, testArgs []string, testEnv map[string]interface{}, isXCTest bool) (appservice.LaunchedAppWithStdIo, error) {
func startTestRunner17(appserviceConn *appservice.Connection, bundleID string, sessionIdentifier string, testBundlePath string, testArgs []string, testEnv map[string]interface{}, isXCTest bool) (appservice.LaunchedAppWithStdIo, error) {
args := []interface{}{}
for _, arg := range testArgs {
args = append(args, arg)
Expand Down Expand Up @@ -483,8 +463,9 @@ func startTestRunner17(device ios.DeviceEntry, appserviceConn *appservice.Connec
}

opts := map[string]interface{}{
"ActivateSuspended": uint64(1),
"StartSuspendedKey": uint64(0),
"ActivateSuspended": uint64(1),
"StartSuspendedKey": uint64(0),
"__ActivateSuspended": uint64(1),
}

appLaunch, err := appserviceConn.LaunchAppWithStdIo(
Expand Down
Loading

0 comments on commit c832f3d

Please sign in to comment.