From 1b7ad4e2eff24cacd2e7cd41d4187e0e8944ceef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20L=C3=BCdeke?= Date: Fri, 11 Oct 2024 12:30:49 +0200 Subject: [PATCH 1/2] allow passing args and envs to app launch --- ios/instruments/processcontrol.go | 18 ++++++++++++++++++ main.go | 29 ++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/ios/instruments/processcontrol.go b/ios/instruments/processcontrol.go index 407434ef..d15790b5 100644 --- a/ios/instruments/processcontrol.go +++ b/ios/instruments/processcontrol.go @@ -31,6 +31,24 @@ func (p *ProcessControl) LaunchApp(bundleID string, my_opts map[string]any) (uin return p.StartProcess(bundleID, env, []interface{}{}, opts) } +// LaunchApp launches the app with the given bundleID on the given device.LaunchApp +// It returns the PID of the created app process. +func (p *ProcessControl) LaunchAppWithArgs(bundleID string, my_args []interface{}, my_env map[string]any, my_opts map[string]any) (uint64, error) { + opts := map[string]interface{}{ + "StartSuspendedKey": uint64(0), + "KillExisting": uint64(0), + } + 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" + // 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"} + maps.Copy(env, my_env) + return p.StartProcess(bundleID, env, my_args, opts) +} + func (p *ProcessControl) Close() error { return p.conn.Close() } diff --git a/main.go b/main.go index fdb0955d..3dfcc6b7 100644 --- a/main.go +++ b/main.go @@ -109,7 +109,7 @@ Usage: ios install --path= [options] ios uninstall [options] ios apps [--system] [--all] [--list] [--filesharing] [options] - ios launch [--wait] [--kill-existing] [options] + ios launch [--wait] [--kill-existing] [--arg=]... [--env=]... [options] ios kill ( | --pid= | --process=) [options] ios runtest [--bundle-id=] [--test-runner-bundle-id=] [--xctest-config=] [--log-output=] [--xctest] [--test-to-run=]... [--test-to-skip=]... [--env=]... [options] ios runwda [--bundleid=] [--testrunnerbundleid=] [--xctestconfig=] [--log-output=] [--arg=]... [--env=]... [options] @@ -221,7 +221,7 @@ The commands work as following: ios install --path= [options] Specify a .app folder or an installable ipa file that will be installed. ios pcap [options] [--pid=] [--process=] Starts a pcap dump of network traffic, use --pid or --process to filter specific processes. ios apps [--system] [--all] [--list] [--filesharing] Retrieves a list of installed applications. --system prints out preinstalled system apps. --all prints all apps, including system, user, and hidden apps. --list only prints bundle ID, bundle name and version number. --filesharing only prints apps which enable documents sharing. - ios launch [--wait] [--kill-existing] [options] Launch app with the bundleID on the device. Get your bundle ID from the apps command. --wait keeps the connection open if you want logs. + ios launch [--wait] [--kill-existing] [--arg=]... [--env=]... [options] Launch app with the bundleID on the device. Get your bundle ID from the apps command. --wait keeps the connection open if you want logs. ios kill ( | --pid= | --process=) [options] Kill app with the specified bundleID, process id, or process name on the device. ios runtest [--bundle-id=] [--test-runner-bundle-id=] [--xctest-config=] [--log-output=] [--xctest] [--test-to-run=]... [--test-to-skip=]... [--env=]... [options] Run a XCUITest. If you provide only bundle-id go-ios will try to dynamically create test-runner-bundle-id and xctest-config. > If you provide '-' as log output, it prints resuts to stdout. @@ -817,7 +817,9 @@ The commands work as following: if bKillExisting { opts["KillExisting"] = 1 } // end if - pid, err := pControl.LaunchApp(bundleID, opts) + args := toArgs(arguments["--arg"].([]string)) + envs := toEnvs(arguments["--env"].([]string)) + pid, err := pControl.LaunchAppWithArgs(bundleID, args, envs, opts) exitIfError("launch app command failed", err) log.WithFields(log.Fields{"pid": pid}).Info("Process launched") if wait { @@ -1277,6 +1279,27 @@ func instrumentsCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { return b } +func toArgs(argsIn []string) []interface{} { + args := []interface{}{} + for _, arg := range argsIn { + args = append(args, arg) + } + return args +} + +func toEnvs(envsIn []string) map[string]interface{} { + env := map[string]interface{}{} + + for _, entrystring := range envsIn { + entry := strings.Split(entrystring, "=") + key := entry[0] + value := entry[1] + env[key] = value + } + + return env +} + func crashCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { b, _ := arguments.Bool("crash") if b { From f3f796b850f61bc54bab7f6db6ff41e3829128bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20L=C3=BCdeke?= Date: Fri, 11 Oct 2024 13:07:58 +0200 Subject: [PATCH 2/2] add integration test --- .../processcontrol_integration_test.go | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/ios/instruments/processcontrol_integration_test.go b/ios/instruments/processcontrol_integration_test.go index 0dba9f02..58ff6fb6 100644 --- a/ios/instruments/processcontrol_integration_test.go +++ b/ios/instruments/processcontrol_integration_test.go @@ -18,10 +18,11 @@ func TestLaunchAndKill(t *testing.T) { } const weatherAppBundleID = "com.apple.weather" pControl, err := instruments.NewProcessControl(device) - defer pControl.Close() if !assert.NoError(t, err) { t.Fatal(err) } + defer pControl.Close() + pid, err := pControl.LaunchApp(weatherAppBundleID, nil) if !assert.NoError(t, err) { return @@ -29,10 +30,56 @@ func TestLaunchAndKill(t *testing.T) { assert.Greater(t, pid, uint64(0)) service, err := instruments.NewDeviceInfoService(device) + if !assert.NoError(t, err) { + return + } defer service.Close() + + processList, err := service.ProcessList() if !assert.NoError(t, err) { return } + found := false + for _, proc := range processList { + if proc.Pid == pid { + found = true + } + } + if !found { + t.Errorf("could not find weather app with pid %d in proclist: %+v", pid, processList) + return + } + err = pControl.KillProcess(pid) + assert.NoError(t, err) +} + +func TestLaunchWithArgsAndKill(t *testing.T) { + device, err := ios.GetDevice("") + if err != nil { + t.Fatal(err) + } + const weatherAppBundleID = "com.apple.weather" + pControl, err := instruments.NewProcessControl(device) + if !assert.NoError(t, err) { + t.Fatal(err) + } + defer pControl.Close() + + var args = []interface{}{"-AppleLanguages", "(de-DE)"} + var env = map[string]interface{}{"SomeRandomValue": "YES"} + + pid, err := pControl.LaunchAppWithArgs(weatherAppBundleID, args, env, nil) + if !assert.NoError(t, err) { + return + } + assert.Greater(t, pid, uint64(0)) + + service, err := instruments.NewDeviceInfoService(device) + if !assert.NoError(t, err) { + return + } + defer service.Close() + processList, err := service.ProcessList() if !assert.NoError(t, err) { return @@ -49,5 +96,4 @@ func TestLaunchAndKill(t *testing.T) { } err = pControl.KillProcess(pid) assert.NoError(t, err) - return }