diff --git a/.github/workflows/real-device.yml b/.github/workflows/real-device.yml index 7ad7fbe8..efb68742 100644 --- a/.github/workflows/real-device.yml +++ b/.github/workflows/real-device.yml @@ -41,11 +41,12 @@ jobs: rm testdata/test1* - name: upload the macos build - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: signed-wda path: testdata/wda-signed.ipa retention-days: 1 + overwrite: true test_on_windows: runs-on: windows-latest @@ -64,7 +65,7 @@ jobs: - name: compile run: go build - name: Download mac signed wda from previous job - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: signed-wda @@ -104,7 +105,7 @@ jobs: cache: true - name: Download mac release from previous job - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: signed-wda diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 196fc395..63ce3345 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,18 +26,19 @@ jobs: - name: Build run: | - ((Get-Content -path main.go -Raw) -replace "local-build","${{ steps.create_release.outputs.current_tag }}") | Set-Content -Path main.go + ((Get-Content -path main.go -Raw) -replace "local-build","${{ steps.create_release.outputs.current_tag }}") | Set-Content -Path main.go mkdir bin go build -ldflags="-s -w" -o bin/ios.exe "${{ steps.create_release.outputs.current_tag }}" | Out-File -Encoding utf8NoBOM release_tag -NoNewline Compress-Archive -Path .\bin\ios.exe, release_tag -CompressionLevel Optimal -DestinationPath go-ios-windows.zip - name: upload the windows build - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: windows-build path: go-ios-windows.zip retention-days: 1 + overwrite: true build_on_mac: runs-on: macos-latest @@ -52,7 +53,7 @@ jobs: cache: true - name: Download win release from previous job - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-build path: ./win-bin @@ -75,11 +76,12 @@ jobs: zip -j go-ios-mac.zip bin/ios - name: upload the macos build - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: macos-build path: go-ios-mac.zip retention-days: 1 + overwrite: true build_on_linux_and_release: runs-on: ubuntu-latest @@ -94,7 +96,7 @@ jobs: cache: true - name: Download mac release from previous job - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: macos-build path: ./mac-bin @@ -107,7 +109,7 @@ jobs: zip -j go-ios-mac.zip ios - name: Download windows release from previous job - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-build path: ./win-bin @@ -124,10 +126,12 @@ jobs: run: | sed -i 's/version \= \"local-build\"/version = \"${{ env.release_tag }}\"/' main.go mkdir bin - go build -ldflags="-s -w" -o bin/ios + GOARCH=arm64 go build -ldflags="-s -w" -o bin/ios-arm64 + GOARCH=amd64 go build -ldflags="-s -w" -o bin/ios-amd64 + cp ./mac-bin/go-ios-mac.zip . cp ./win-bin/go-ios-win.zip . - zip -j go-ios-linux.zip bin/ios + zip -j go-ios-linux.zip bin/ios-arm64 bin/ios-amd64 - uses: AButler/upload-release-assets@v2.0 with: @@ -143,11 +147,13 @@ jobs: mkdir ./npm_publish/dist/go-ios-darwin-amd64_darwin_amd64 mkdir ./npm_publish/dist/go-ios-darwin-arm64_darwin_arm64 mkdir ./npm_publish/dist/go-ios-linux-amd64_linux_amd64 + mkdir ./npm_publish/dist/go-ios-linux-arm64_linux_arm64 mkdir ./npm_publish/dist/go-ios-windows-amd64_windows_amd64 cp ./mac-bin/ios ./npm_publish/dist/go-ios-darwin-amd64_darwin_amd64/ios cp ./mac-bin/ios ./npm_publish/dist/go-ios-darwin-arm64_darwin_arm64/ios cp ./win-bin/ios.exe ./npm_publish/dist/go-ios-windows-amd64_windows_amd64/ios.exe - cp ./bin/ios ./npm_publish/dist/go-ios-linux-amd64_linux_amd64/ios + cp ./bin/ios-amd64 ./npm_publish/dist/go-ios-linux-amd64_linux_amd64/ios + cp ./bin/ios-arm64 ./npm_publish/dist/go-ios-linux-arm64_linux_arm64/ios echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc cd npm_publish sed -i 's/\"local-build\"/\"${{ env.release_tag }}\"/' package.json diff --git a/Makefile b/Makefile index a29cecdf..58377eb3 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index cf06afdf..821ad522 100644 --- a/README.md +++ b/README.md @@ -43,87 +43,129 @@ All features: ``` Options: - -v --verbose Enable Debug Logging. - -t --trace Enable Trace Logging (dump every message). - --nojson Disable JSON output (default). - -h --help Show this screen. - --udid= UDID of the device. + -v --verbose Enable Debug Logging. + -t --trace Enable Trace Logging (dump every message). + --nojson Disable JSON output + --pretty Pretty-print JSON command output + -h --help Show this screen. + --udid= UDID of the device. + --tunnel-info-port= When go-ios is used to manage tunnels for iOS 17+ it exposes them on an HTTP-API for localhost (default port: 28100) + --address= Address of the device on the interface. This parameter is optional and can be set if a tunnel created by MacOS needs to be used. + > To get this value run "log stream --debug --info --predicate 'eventMessage LIKE "*Tunnel established*" OR eventMessage LIKE "*for server port*"'", + > connect a device and open Xcode + --rsd-port= Port of remote service discovery on the device through the tunnel + > This parameter is similar to '--address' and can be obtained by the same log filter + --proxyurl= Set this if you want go-ios to use a http proxy for outgoing requests, like for downloading images or contacting Apple during device activation. + > A simple format like: "http://PROXY_LOGIN:PROXY_PASS@proxyIp:proxyPort" works. Otherwise use the HTTP_PROXY system env var. + --userspace-port= Optional. Set this if you run a command supplying rsd-port and address and your device is using userspace tunnel The commands work as following: The default output of all commands is JSON. Should you prefer human readable outout, specify the --nojson option with your command. By default, the first device found will be used for a command unless you specify a --udid=some_udid switch. Specify -v for debug logging and -t for dumping every message. -ios listen [options] Keeps a persistent connection open and notifies about newly connected or disconnected devices. - ios list [options] [--details] Prints a list of all connected device's udids. If --details is specified, it includes version, name and model of each device. - ios info [options] Prints a dump of Lockdown getValues. - ios image list [options] List currently mounted developers images' signatures - ios image mount [--path=] [options] Mount a image from - ios image auto [--basedir=] [options] Automatically download correct dev image from the internets and mount it. - > You can specify a dir where images should be cached. - > The default is the current dir. - ios syslog [options] Prints a device's log output - ios screenshot [options] [--output=] Takes a screenshot and writes it to the current dir or to - ios instruments notifications [options] Listen to application state notifications + ios --version | version [options] Prints the version + ios -h | --help Prints this screen. + ios activate [options] Activate a device + 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 assistivetouch (enable | disable | toggle | get) [--force] [options] Enables, disables, toggles, or returns the state of the "AssistiveTouch" software home-screen button. iOS 11+ only (Use --force to try on older versions). + ios ax [--font=] [options] Access accessibility inspector features. + ios batterycheck [options] Prints battery info. + ios batteryregistry [options] Prints battery registry stats like Temperature, Voltage. + ios crash cp [options] copy "file pattern" to the target dir. Ex.: 'ios crash cp "*" "./crashes"' ios crash ls [] [options] run "ios crash ls" to get all crashreports in a list, > or use a pattern like 'ios crash ls "*ips*"' to filter - ios crash cp [options] copy "file pattern" to the target dir. Ex.: 'ios crash cp "*" "./crashes"' ios crash rm [options] remove file pattern from dir. Ex.: 'ios crash rm "." "*"' to delete everything - ios devicename [options] Prints the devicename ios date [options] Prints the device date - ios devicestate list [options] Prints a list of all supported device conditions, like slow network, gpu etc. + ios debug [--stop-at-entry] Start debug with lldb + ios devicename [options] Prints the devicename ios devicestate enable [options] Enables a profile with ids (use the list command to see options). It will only stay active until the process is terminated. > Ex. "ios devicestate enable SlowNetworkCondition SlowNetwork3GGood" - ios lang [--setlocale=] [--setlang=] [options] Sets or gets the Device language + ios devicestate list [options] Prints a list of all supported device conditions, like slow network, gpu etc. + ios devmode (enable | get) [--enable-post-restart] [options] Enable developer mode on the device or check if it is enabled. Can also completely finalize developer mode setup after device is restarted. + ios diagnostics list [options] List diagnostic infos + ios diskspace [options] Prints disk space info. + ios dproxy [--binary] [--mode=] [--iface=] [options] Starts the reverse engineering proxy server. + > It dumps every communication in plain text so it can be implemented easily. + > Use "sudo launchctl unload -w /Library/Apple/System/Library/LaunchDaemons/com.apple.usbmuxd.plist" + > to stop usbmuxd and load to start it again should the proxy mess up things. + > The --binary flag will dump everything in raw binary without any decoding. + ios erase [--force] [options] Erase the device. It will prompt you to input y+Enter unless --force is specified. + ios forward [options] Similar to iproxy, forward a TCP connection to the device. + ios fsync [--app=bundleId] [options] (pull | push) --srcPath= --dstPath= Pull or Push file from srcPath to dstPath. + ios fsync [--app=bundleId] [options] (rm [--r] | tree | mkdir) --path= Remove | treeview | mkdir in target path. --r used alongside rm will recursively remove all files and directories from target path. + ios httpproxy [] [] --p12file= [--password=] set global http proxy on supervised device. Use the password argument or set the environment variable 'P12_PASSWORD' + > Specify proxy password either as argument or using the environment var: PROXY_PASSWORD + > Use p12 file and password for silent installation on supervised devices. + ios httpproxy remove [options] Removes the global http proxy config. Only works with http proxies set by go-ios! + ios image auto [--basedir=] [options] Automatically download correct dev image from the internets and mount it. + > You can specify a dir where images should be cached. + > The default is the current dir. + ios image list [options] List currently mounted developers images' signatures + ios image mount [--path=] [options] Mount a image from + > For iOS 17+ (personalized developer disk images) must point to the "Restore" directory inside the developer disk + ios image unmount [options] Unmount developer disk image + ios info [display | lockdown] [options] Prints a dump of device information from the given source. + ios install --path= [options] Specify a .app folder or an installable ipa file that will be installed. + ios instruments notifications [options] Listen to application state notifications + ios ip [options] Uses the live pcap iOS packet capture to wait until it finds one that contains the IP address of the device. + > It relies on the MAC address of the WiFi adapter to know which is the right IP. + > You have to disable the "automatic wifi address"-privacy feature of the device for this to work. + > If you wanna speed it up, open apple maps or similar to force network traffic. + > f.ex. "ios launch com.apple.Maps" + ios kill ( | --pid= | --process=) [options] Kill app with the specified bundleID, process id, or process name on the device. + ios lang [--setlocale=] [--setlang=] [options] Sets or gets the Device language. ios lang will print the current language and locale, as well as a list of all supported langs and locales. + 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 list [options] [--details] Prints a list of all connected device's udids. If --details is specified, it includes version, name and model of each device. + ios listen [options] Keeps a persistent connection open and notifies about newly connected or disconnected devices. + ios memlimitoff (--process=) [options] Waives memory limit set by iOS (For instance a Broadcast Extension limit is 50 MB). ios mobilegestalt ... [--plist] [options] Lets you query mobilegestalt keys. Standard output is json but if desired you can get > it in plist format by adding the --plist param. > Ex.: "ios mobilegestalt MainScreenCanvasSizes ArtworkTraits --plist" - ios diagnostics list [options] List diagnostic infos ios pair [--p12file=] [--password=] [options] Pairs the device. If the device is supervised, specify the path to the p12 file > to pair without a trust dialog. Specify the password either with the argument or > by setting the environment variable 'P12_PASSWORD' + ios pcap [options] [--pid=] [--process=] Starts a pcap dump of network traffic, use --pid or --process to filter specific processes. + ios prepare [--skip-all] [--skip=]... [--env=]...[options] runs WebDriverAgents - > specify runtime args and env vars like --env ENV_1=something --env ENV_2=else and --arg ARG1 --arg ARG2 - ios ax [options] Access accessibility inspector features. - ios debug [--stop-at-entry] Start debug with lldb - ios fsync (rm [--r] | tree | mkdir) --path= Remove | treeview | mkdir in target path. --r used alongside rm will recursively remove all files and directories from target path. - ios fsync (pull | push) --srcPath= --dstPath= Pull or Push file from srcPath to dstPath. ios reboot [options] Reboot the given device - ios -h | --help Prints this screen. - ios --version | version [options] Prints the version + ios resetax [options] Reset accessibility settings to defaults. + ios resetlocation [options] Resets the location of the device to the actual one + ios rsd ls [options] List RSD services and their port. + 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. + > To be able to filter for tests to run or skip, use one argument per test selector. Example: runtest --test-to-run=(TestTarget.)TestClass/testMethod --test-to-run=(TestTarget.)TestClass/testMethod (the value for 'TestTarget' is optional) + > The method name can also be omitted and in this case all tests of the specified class are run + ios runwda [--bundleid=] [--testrunnerbundleid=] [--xctestconfig=] [--log-output=] [--arg=]... [--env=]...[options] runs WebDriverAgents + > specify runtime args and env vars like --env ENV_1=something --env ENV_2=else and --arg ARG1 --arg ARG2 + ios runxctest [--xctestrun-file-path=] [--log-output=] [options] Run a XCTest. The --xctestrun-file-path specifies the path to the .xctestrun file to configure the test execution. + > If you provide '-' as log output, it prints resuts to stdout. + ios screenshot [options] [--output=] [--stream] [--port=] Takes a screenshot and writes it to the current dir or to If --stream is supplied it + > starts an mjpeg server at 0.0.0.0:3333. Use --port to set another port. ios setlocation [options] [--lat=] [--lon=] Updates the location of the device to the provided by latitude and longitude coordinates. Example: setlocation --lat=40.730610 --lon=-73.935242 ios setlocationgpx [options] [--gpxfilepath=] Updates the location of the device based on the data in a GPX file. Example: setlocationgpx --gpxfilepath=/home/username/location.gpx - ios resetlocation [options] Resets the location of the device to the actual one - ios assistivetouch (enable | disable | toggle | get) [--force] [options] Enables, disables, toggles, or returns the state of the "AssistiveTouch" software home-screen button. iOS 11+ only (Use --force to try on older versions). - ios diskspace [options] Prints disk space info. + ios syslog [--parse] [options] Prints a device's log output, Use --parse to parse the fields from the log + ios sysmontap Get system stats like MEM, CPU + ios timeformat (24h | 12h | toggle | get) [--force] [options] Sets, or returns the state of the "time format". iOS 11+ only (Use --force to try on older versions). + ios tunnel ls List currently started tunnels. Use --enabletun to activate using TUN devices rather than user space network. Requires sudo/admin shells. + ios tunnel start [options] [--pair-record-path=] [--enabletun] Creates a tunnel connection to the device. If the device was not paired with the host yet, device pairing will also be executed. + > On systems with System Integrity Protection enabled the argument '--pair-record-path=default' can be used to point to /var/db/lockdown/RemotePairing/user_501. + > If nothing is specified, the current dir is used for the pair record. + > This command needs to be executed with admin privileges. + > (On MacOS the process 'remoted' must be paused before starting a tunnel is possible 'sudo pkill -SIGSTOP remoted', and 'sudo pkill -SIGCONT remoted' to resume) + ios voiceover (enable | disable | toggle | get) [--force] [options] Enables, disables, toggles, or returns the state of the "VoiceOver" software home-screen button. iOS 11+ only (Use --force to try on older versions). + ios zoom (enable | disable | toggle | get) [--force] [options] Enables, disables, toggles, or returns the state of the "ZoomTouch" software home-screen button. iOS 11+ only (Use --force to try on older versions). ``` diff --git a/go.work b/go.work index dbdec784..a60576bd 100644 --- a/go.work +++ b/go.work @@ -5,4 +5,5 @@ toolchain go1.22.5 use ( . ./ncm + ./restapi ) diff --git a/go.work.sum b/go.work.sum index dc019e6f..6253bf1f 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,100 +1,188 @@ +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/Microsoft/hcsshim v0.8.14 h1:lbPVK25c1cu5xTLITwpUcxoA9vKrKErASPYygvouJns= github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs= +github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU= github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/bazelbuild/rules_go v0.44.2 h1:H2nzlC9VLKeVW1D90bahFSszpDE5qvtKr95Nz7BN0WQ= github.com/bazelbuild/rules_go v0.44.2/go.mod h1:Dhcz716Kqg1RHNWos+N6MlXNkjNP2EwZQ0LukRKJfMs= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= +github.com/containerd/cgroups v1.0.1 h1:iJnMvco9XGvKUvNQkv88bE4uJXxRQH18efbKo9w5vHQ= github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= +github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc= github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/containerd/containerd v1.4.13 h1:Z0CbagVdn9VN4K6htOCY/jApSw8YKP+RdLZ5dkXF8PM= github.com/containerd/containerd v1.4.13/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/containerd/fifo v1.0.0 h1:6PirWBr9/L7GDamKr+XM0IeUFXu5mf3M/BPpH9gaLBU= github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/go-runc v1.0.0 h1:oU+lLv1ULm5taqgV/CJivypVODI4SUz1znWjv3nNYS0= github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/ttrpc v1.1.0 h1:GbtyLRxb0gOLR0TYQWt3O6B0NvT8tMdorEHqIQo/lWI= github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= +github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY= github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4= github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.0.2-0.20190508160503-636abe8753b8 h1:8nlgEAjIalk6uj/CGKCdOO8CQqTeysvcW4RFZ6HbkGM= github.com/google/subcommands v1.0.2-0.20190508160503-636abe8753b8/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/hanwen/go-fuse/v2 v2.3.0 h1:t5ivNIH2PK+zw4OBul/iJjsoG9K6kXo4nMDoBpciC8A= github.com/hanwen/go-fuse/v2 v2.3.0/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a h1:+J2gw7Bw77w/fbK7wnNJJDKmw1IbWft2Ul5BzrG1Qm8= github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170308212314-bb9b5e7adda9 h1:Sha2bQdoWE5YQPTlJOL31rmce94/tYi113SlFo1xQ2c= github.com/mohae/deepcopy v0.0.0-20170308212314-bb9b5e7adda9/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/runtime-spec v1.1.0-rc.1 h1:wHa9jroFfKGQqFHj0I1fMRKLl0pfj+ynAqBxo3v6u9w= github.com/opencontainers/runtime-spec v1.1.0-rc.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE= +github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI= +github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So= github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.53.0-dev.0.20230123225046-4075ef07c5d5 h1:qq9WB3Dez2tMAKtZTVtZsZSmTkDgPeXx+FRPt5kLEkM= google.golang.org/grpc v1.53.0-dev.0.20230123225046-4075ef07c5d5/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= +honnef.co/go/tools v0.4.2 h1:6qXr+R5w+ktL5UkwEbPp+fEvfyoMPche6GkOpGHZcLc= honnef.co/go/tools v0.4.2/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA= +k8s.io/api v0.23.16 h1:op+yeqZLQxDt2tEnrOP9Y+WA7l4Lxh+7R0IWEzyuk2I= k8s.io/api v0.23.16/go.mod h1:Fk/eWEGf3ZYZTCVLbsgzlxekG6AtnT3QItT3eOSyFRE= +k8s.io/apimachinery v0.23.16 h1:f6Q+3qYv3qWvbDZp2iUhwC2rzMRBkSb7JYBhmeVK5pc= k8s.io/apimachinery v0.23.16/go.mod h1:RMMUoABRwnjoljQXKJ86jT5FkTZPPnZsNv70cMsKIP0= +k8s.io/client-go v0.23.16 h1:9NyRabEbkE9/7Rc3ZI8kMYfH3kocUD+wEBifaTn6lyU= k8s.io/client-go v0.23.16/go.mod h1:CUfIIQL+hpzxnD9nxiVGb99BNTp00mPFp3Pk26sTFys= +k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/ios/accessibility/accessibility.go b/ios/accessibility/accessibility.go index 5b900cae..cb28f908 100644 --- a/ios/accessibility/accessibility.go +++ b/ios/accessibility/accessibility.go @@ -7,6 +7,17 @@ import ( const serviceName string = "com.apple.accessibility.axAuditDaemon.remoteserver" +// NewWithoutEventChangeListeners creates and connects to the given device, a new ControlInterface instance +// without setting accessibility event change listeners to avoid keeping constant connection. +func NewWithoutEventChangeListeners(device ios.DeviceEntry) (ControlInterface, error) { + conn, err := dtx.NewUsbmuxdConnection(device, serviceName) + if err != nil { + return ControlInterface{}, err + } + control := ControlInterface{conn.GlobalChannel()} + return control, nil +} + // New creates and connects to the given device, a new ControlInterface instance func New(device ios.DeviceEntry) (ControlInterface, error) { conn, err := dtx.NewUsbmuxdConnection(device, serviceName) diff --git a/ios/accessibility/accessibility_control.go b/ios/accessibility/accessibility_control.go index 5ebea008..1defae48 100644 --- a/ios/accessibility/accessibility_control.go +++ b/ios/accessibility/accessibility_control.go @@ -38,7 +38,7 @@ func (a ControlInterface) readhostInspectorNotificationReceived() { } } -// Init wires up event receivers and gets Info from the device +// init wires up event receivers and gets Info from the device func (a ControlInterface) init() error { a.channel.RegisterMethodForRemote("hostInspectorCurrentElementChanged:") a.channel.RegisterMethodForRemote("hostInspectorMonitoredEventTypeChanged:") @@ -150,6 +150,14 @@ func (a ControlInterface) UpdateAccessibilitySetting(name string, val interface{ log.Info("Setting Updated", resp) } +func (a ControlInterface) ResetToDefaultAccessibilitySettings() error { + err := a.channel.MethodCallAsync("deviceResetToDefaultAccessibilitySettings") + if err != nil { + return err + } + return nil +} + func (a ControlInterface) awaitHostInspectorCurrentElementChanged() map[string]interface{} { msg := a.channel.ReceiveMethodCall("hostInspectorCurrentElementChanged:") log.Info("received hostInspectorCurrentElementChanged") @@ -194,7 +202,7 @@ func (a ControlInterface) deviceCapabilities() ([]string, error) { if err != nil { return nil, err } - return convertToStringList(response.Payload), nil + return convertToStringList(response.Payload) } func (a ControlInterface) deviceAllAuditCaseIDs() ([]string, error) { @@ -202,7 +210,7 @@ func (a ControlInterface) deviceAllAuditCaseIDs() ([]string, error) { if err != nil { return nil, err } - return convertToStringList(response.Payload), nil + return convertToStringList(response.Payload) } func (a ControlInterface) deviceAccessibilitySettings() (map[string]interface{}, error) { diff --git a/ios/accessibility/utils.go b/ios/accessibility/utils.go index 1999ad98..15c09f65 100644 --- a/ios/accessibility/utils.go +++ b/ios/accessibility/utils.go @@ -1,10 +1,15 @@ package accessibility -func convertToStringList(payload []interface{}) []string { +import "fmt" + +func convertToStringList(payload []interface{}) ([]string, error) { + if len(payload) != 1 { + return nil, fmt.Errorf("invalid payload length %d", len(payload)) + } list := payload[0].([]interface{}) result := make([]string, len(list)) for i, v := range list { result[i] = v.(string) } - return result + return result, nil } diff --git a/ios/afc/fsync.go b/ios/afc/fsync.go index e049cc11..eff493aa 100644 --- a/ios/afc/fsync.go +++ b/ios/afc/fsync.go @@ -3,6 +3,7 @@ package afc import ( "bytes" "encoding/binary" + "errors" "fmt" "io" "os" @@ -13,6 +14,7 @@ import ( "github.com/danielpaulus/go-ios/ios" log "github.com/sirupsen/logrus" + "howett.net/plist" ) const serviceName = "com.apple.afc" @@ -48,6 +50,67 @@ func New(device ios.DeviceEntry) (*Connection, error) { return &Connection{deviceConn: deviceConn}, nil } +func NewContainer(device ios.DeviceEntry, bundleID string) (*Connection, error) { + deviceConn, err := ios.ConnectToService(device, "com.apple.mobile.house_arrest") + if err != nil { + return nil, err + } + err = VendContainer(deviceConn, bundleID) + if err != nil { + return nil, err + } + return &Connection{deviceConn: deviceConn}, nil +} + +func VendContainer(deviceConn ios.DeviceConnectionInterface, bundleID string) error { + plistCodec := ios.NewPlistCodec() + vendContainer := map[string]interface{}{"Command": "VendContainer", "Identifier": bundleID} + msg, err := plistCodec.Encode(vendContainer) + if err != nil { + return fmt.Errorf("VendContainer Encoding cannot fail unless the encoder is broken: %v", err) + } + err = deviceConn.Send(msg) + if err != nil { + return err + } + reader := deviceConn.Reader() + response, err := plistCodec.Decode(reader) + if err != nil { + return err + } + return checkResponse(response) +} + +func checkResponse(vendContainerResponseBytes []byte) error { + response, err := plistFromBytes(vendContainerResponseBytes) + if err != nil { + return err + } + if "Complete" == response.Status { + return nil + } + if response.Error != "" { + return errors.New(response.Error) + } + return errors.New("unknown error during vendcontainer") +} + +func plistFromBytes(plistBytes []byte) (vendContainerResponse, error) { + var vendResponse vendContainerResponse + decoder := plist.NewDecoder(bytes.NewReader(plistBytes)) + + err := decoder.Decode(&vendResponse) + if err != nil { + return vendResponse, err + } + return vendResponse, nil +} + +type vendContainerResponse struct { + Status string + Error string +} + // NewFromConn allows to use AFC on a DeviceConnectionInterface, see crashreport for an example func NewFromConn(deviceConn ios.DeviceConnectionInterface) *Connection { return &Connection{deviceConn: deviceConn} diff --git a/ios/connect.go b/ios/connect.go index 2a388a6f..516b2b54 100755 --- a/ios/connect.go +++ b/ios/connect.go @@ -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) @@ -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 @@ -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 +} diff --git a/ios/debugserver/debugserver.go b/ios/debugserver/debugserver.go index c5bb1a2a..877322a8 100644 --- a/ios/debugserver/debugserver.go +++ b/ios/debugserver/debugserver.go @@ -176,8 +176,8 @@ func Start(device ios.DeviceEntry, appPath string, stopAtEntry bool) error { } var container string for _, ai := range appinfo { - if ai.CFBundleIdentifier == bundleId { - container = ai.Path + if ai.CFBundleIdentifier() == bundleId { + container = ai.Path() break } } diff --git a/ios/diagnostics/diagnostics.go b/ios/diagnostics/diagnostics.go index 52d7452a..32f536c3 100644 --- a/ios/diagnostics/diagnostics.go +++ b/ios/diagnostics/diagnostics.go @@ -33,6 +33,27 @@ func Reboot(device ios.DeviceEntry) error { return service.Close() } +// Battery extracts the battery ioregistry stats like Temperature, Voltage, CurrentCapacity +func (diagnosticsConn *Connection) Battery() (IORegistry, error) { + req := newIORegistryRequest() + req.addClass("IOPMPowerSource") + + reader := diagnosticsConn.deviceConn.Reader() + encoded, err := req.encoded() + if err != nil { + return IORegistry{}, err + } + err = diagnosticsConn.deviceConn.Send(encoded) + if err != nil { + return IORegistry{}, err + } + response, err := diagnosticsConn.plistCodec.Decode(reader) + if err != nil { + return IORegistry{}, err + } + return diagnosticsfromBytes(response).Diagnostics.IORegistry, nil +} + func (diagnosticsConn *Connection) Reboot() error { req := rebootRequest{Request: "Restart", WaitForDisconnect: true, DisplayFail: true, DisplayPass: true} reader := diagnosticsConn.deviceConn.Reader() diff --git a/ios/diagnostics/ioregistry.go b/ios/diagnostics/ioregistry.go index 551ce2ac..c05d2a12 100644 --- a/ios/diagnostics/ioregistry.go +++ b/ios/diagnostics/ioregistry.go @@ -2,27 +2,32 @@ package diagnostics import ios "github.com/danielpaulus/go-ios/ios" -func ioregentryRequest(key string) []byte { - requestMap := map[string]interface{}{ - "Request": "IORegistry", - "EntryName": key, - } - bt, err := ios.PlistCodec{}.Encode(requestMap) - if err != nil { - panic("query request encoding should never fail") - } - return bt +type ioregistryRequest struct { + reqMap map[string]string } -func (diagnosticsConn *Connection) IORegEntryQuery(key string) (interface{}, error) { - err := diagnosticsConn.deviceConn.Send(ioregentryRequest(key)) - if err != nil { - return "", err - } - respBytes, err := diagnosticsConn.plistCodec.Decode(diagnosticsConn.deviceConn.Reader()) +func newIORegistryRequest() *ioregistryRequest { + return &ioregistryRequest{map[string]string{ + "Request": "IORegistry", + }} +} + +func (req *ioregistryRequest) addPlane(plane string) { + req.reqMap["CurrentPlane"] = plane +} + +func (req *ioregistryRequest) addName(name string) { + req.reqMap["EntryName"] = name +} + +func (req *ioregistryRequest) addClass(class string) { + req.reqMap["EntryClass"] = class +} + +func (req *ioregistryRequest) encoded() ([]byte, error) { + bt, err := ios.PlistCodec{}.Encode(req.reqMap) if err != nil { - return "", err + return nil, err } - plist, err := ios.ParsePlist(respBytes) - return plist, err + return bt, nil } diff --git a/ios/diagnostics/request.go b/ios/diagnostics/request.go index 7fe07538..32960f9c 100644 --- a/ios/diagnostics/request.go +++ b/ios/diagnostics/request.go @@ -30,10 +30,26 @@ type allDiagnosticsResponse struct { } type Diagnostics struct { - GasGauge GasGauge - HDMI HDMI - NAND NAND - WiFi WiFi + GasGauge GasGauge + HDMI HDMI + NAND NAND + WiFi WiFi + IORegistry IORegistry +} + +// IORegistry relates to the battery stats +type IORegistry struct { + InstantAmperage int + Temperature int + Voltage int + IsCharging bool + CurrentCapacity int + + DesignCapacity uint64 + NominalChargeCapacity uint64 + CycleCount uint64 + AtCriticalLevel bool + AtWarnLevel bool } type WiFi struct { diff --git a/ios/dtx_codec/channel.go b/ios/dtx_codec/channel.go index 700b7973..bba99765 100644 --- a/ios/dtx_codec/channel.go +++ b/ios/dtx_codec/channel.go @@ -49,19 +49,31 @@ func (d *Channel) ReceiveMethodCall(selector string) Message { // MethodCall is the standard DTX style remote method invocation pattern. The ObjectiveC Selector goes as a NSKeyedArchiver.archived NSString into the // DTXMessage payload, and the arguments are separately NSKeyArchiver.archived and put into the Auxiliary DTXPrimitiveDictionary. It returns the response message and an error. func (d *Channel) MethodCall(selector string, args ...interface{}) (Message, error) { - payload, _ := nskeyedarchiver.ArchiveBin(selector) auxiliary := NewPrimitiveDictionary() for _, arg := range args { auxiliary.AddNsKeyedArchivedObject(arg) } + + return d.methodCallWithReply(selector, auxiliary) +} + +// MethodCallWithAuxiliary is a DTX style remote method invocation pattern. The ObjectiveC Selector goes as a NSKeyedArchiver.archived NSString into the +// DTXMessage payload, and the primitive arguments put into the Auxiliary DTXPrimitiveDictionary. It returns the response message and an error. +func (d *Channel) MethodCallWithAuxiliary(selector string, aux PrimitiveDictionary) (Message, error) { + return d.methodCallWithReply(selector, aux) +} + +func (d *Channel) methodCallWithReply(selector string, auxiliary PrimitiveDictionary) (Message, error) { + payload, _ := nskeyedarchiver.ArchiveBin(selector) msg, err := d.SendAndAwaitReply(true, Methodinvocation, payload, auxiliary) if err != nil { log.WithFields(log.Fields{"channel_id": d.channelName, "error": err, "methodselector": selector}).Info("failed starting invoking method") return msg, err } if msg.HasError() { - return msg, fmt.Errorf("Failed invoking method '%s' with error: %s", selector, msg.Payload[0]) + return msg, fmt.Errorf("failed invoking method '%s' with error: %s", selector, msg.Payload[0]) } + return msg, nil } diff --git a/ios/dtx_codec/connection.go b/ios/dtx_codec/connection.go index ebec76da..f2b1cdb5 100644 --- a/ios/dtx_codec/connection.go +++ b/ios/dtx_codec/connection.go @@ -30,6 +30,14 @@ type Connection struct { mutex sync.Mutex requestChannelMessages chan Message + // MessageDispatcher use this prop to catch messages from GlobalDispatcher + // and handle it accordingly in a custom dispatcher of the dedicated service + // + // Set this prop when creating a connection instance + // + // Refer to end-to-end example of `instruments/instruments_sysmontap.go` + MessageDispatcher Dispatcher + closed chan struct{} err error closeOnce sync.Once @@ -87,6 +95,18 @@ func NewGlobalDispatcher(requestChannelMessages chan Message, dtxConnection *Con return dispatcher } +// Dispatch to a MessageDispatcher of the Connection if set +func (dtxConn *Connection) Dispatch(msg Message) { + msgDispatcher := dtxConn.MessageDispatcher + if msgDispatcher != nil { + log.Debugf("msg dispatcher found: %T", msgDispatcher) + msgDispatcher.Dispatch(msg) + return + } + + log.Errorf("no connection dispatcher registered for global channel, msg: %v", msg) +} + // Dispatch prints log messages and errors when they are received and also creates local Channels when requested by the device. func (g GlobalDispatcher) Dispatch(msg Message) { SendAckIfNeeded(g.dtxConnection, msg) @@ -102,7 +122,7 @@ func (g GlobalDispatcher) Dispatch(msg Message) { "msg": logmsg[0], "pid": msg.Auxiliary.GetArguments()[1], "time": msg.Auxiliary.GetArguments()[2], - }).Info("outputReceived:fromProcess:atTime:") + }).Debug("outputReceived:fromProcess:atTime:") } return } @@ -111,6 +131,9 @@ func (g GlobalDispatcher) Dispatch(msg Message) { if msg.HasError() { log.Error(msg.Payload[0]) } + if msg.PayloadHeader.MessageType == UnknownTypeOne || msg.PayloadHeader.MessageType == ResponseWithReturnValueInPayload { + g.dtxConnection.Dispatch(msg) + } } func notifyOfPublishedCapabilities(msg Message) { diff --git a/ios/house_arrest/house_arrest.go b/ios/house_arrest/house_arrest.go index 6ea340da..605d5c0c 100644 --- a/ios/house_arrest/house_arrest.go +++ b/ios/house_arrest/house_arrest.go @@ -1,17 +1,13 @@ package house_arrest import ( - "bytes" "encoding/binary" - "errors" "fmt" "strings" "github.com/danielpaulus/go-ios/ios/afc" "github.com/danielpaulus/go-ios/ios" - - "howett.net/plist" ) const serviceName = "com.apple.mobile.house_arrest" @@ -26,62 +22,13 @@ func New(device ios.DeviceEntry, bundleID string) (*Connection, error) { if err != nil { return &Connection{}, err } - err = vendContainer(deviceConn, bundleID) + err = afc.VendContainer(deviceConn, bundleID) if err != nil { return &Connection{}, err } return &Connection{deviceConn: deviceConn}, nil } -func vendContainer(deviceConn ios.DeviceConnectionInterface, bundleID string) error { - plistCodec := ios.NewPlistCodec() - vendContainer := map[string]interface{}{"Command": "VendContainer", "Identifier": bundleID} - msg, err := plistCodec.Encode(vendContainer) - if err != nil { - return fmt.Errorf("VendContainer Encoding cannot fail unless the encoder is broken: %v", err) - } - err = deviceConn.Send(msg) - if err != nil { - return err - } - reader := deviceConn.Reader() - response, err := plistCodec.Decode(reader) - if err != nil { - return err - } - return checkResponse(response) -} - -func checkResponse(vendContainerResponseBytes []byte) error { - response, err := plistFromBytes(vendContainerResponseBytes) - if err != nil { - return err - } - if "Complete" == response.Status { - return nil - } - if response.Error != "" { - return errors.New(response.Error) - } - return errors.New("unknown error during vendcontainer") -} - -func plistFromBytes(plistBytes []byte) (vendContainerResponse, error) { - var vendResponse vendContainerResponse - decoder := plist.NewDecoder(bytes.NewReader(plistBytes)) - - err := decoder.Decode(&vendResponse) - if err != nil { - return vendResponse, err - } - return vendResponse, nil -} - -type vendContainerResponse struct { - Status string - Error string -} - func (c Connection) Close() { if c.deviceConn != nil { c.deviceConn.Close() @@ -133,7 +80,7 @@ func (conn *Connection) openFileForWriting(filePath string) (byte, error) { return 0, err } if response.Header.Operation != afc.Afc_operation_file_open_result { - return 0, fmt.Errorf("Unexpected afc response, expected %x received %x", afc.Afc_operation_status, response.Header.Operation) + return 0, fmt.Errorf("unexpected afc response, expected %x received %x", afc.Afc_operation_status, response.Header.Operation) } return response.HeaderPayload[0], nil } @@ -157,7 +104,7 @@ func (conn *Connection) sendFileContents(fileContents []byte, handle byte) error return err } if response.Header.Operation != afc.Afc_operation_status { - return fmt.Errorf("Unexpected afc response, expected %x received %x", afc.Afc_operation_status, response.Header.Operation) + return fmt.Errorf("unexpected afc response, expected %x received %x", afc.Afc_operation_status, response.Header.Operation) } return nil } @@ -174,7 +121,7 @@ func (conn *Connection) closeHandle(handle byte) error { return err } if response.Header.Operation != afc.Afc_operation_status { - return fmt.Errorf("Unexpected afc response, expected %x received %x", afc.Afc_operation_status, response.Header.Operation) + return fmt.Errorf("unexpected afc response, expected %x received %x", afc.Afc_operation_status, response.Header.Operation) } return nil } diff --git a/ios/imagemounter/imagemounter.go b/ios/imagemounter/imagemounter.go index f105c060..cf6f9baa 100644 --- a/ios/imagemounter/imagemounter.go +++ b/ios/imagemounter/imagemounter.go @@ -287,6 +287,7 @@ func IsDevModeEnabled(device ios.DeviceEntry) (bool, error) { if err != nil { return false, fmt.Errorf("IsDevModeEnabled: failed connecting to image mounter service with err: %w", err) } + defer conn.Close() reader := conn.Reader() request := map[string]interface{}{"Command": "QueryDeveloperModeStatus"} diff --git a/ios/installationproxy/installationproxy.go b/ios/installationproxy/installationproxy.go index 19e26c09..9feac664 100644 --- a/ios/installationproxy/installationproxy.go +++ b/ios/installationproxy/installationproxy.go @@ -12,6 +12,31 @@ import ( const serviceName = "com.apple.mobile.installation_proxy" +const ( + // ApplicationType shows if this is a 'User', 'System' or 'Hidden' app + ApplicationType = "ApplicationType" + CFBundleDisplayName = "CFBundleDisplayName" + CFBundleExecutable = "CFBundleExecutable" + CFBundleIdentifier = "CFBundleIdentifier" + CFBundleName = "CFBundleName" + CFBundleNumericVersion = "CFBundleNumericVersion" + CFBundleShortVersionString = "CFBundleShortVersionString" + CFBundleSupportedPlatforms = "CFBundleSupportedPlatforms" + CFBundleVersion = "CFBundleVersion" + // DTXcode is the Xcode version the app was built with (e.g. Xcode 16.4 is '1640') + DTXcode = "DTXcode" + // DTXcodeBuild is the Xcode build version the app was built with + DTXcodeBuild = "DTXcodeBuild" + Entitlements = "Entitlements" + EnvironmentVariables = "EnvironmentVariables" + // MinimumOSVersion defines the minimum supported iOS version + MinimumOSVersion = "MinimumOSVersion" + Path = "Path" + // UIDeviceFamily slice of integers for supported devices types where a value of '1' means iPhone, and '2' iPad + UIDeviceFamily = "UIDeviceFamily" + UIFileSharingEnabled = "UIFileSharingEnabled" +) + type Connection struct { deviceConn ios.DeviceConnectionInterface plistCodec ios.PlistCodec @@ -134,30 +159,7 @@ func plistFromBytes(plistBytes []byte) (BrowseResponse, error) { } func browseApps(applicationType string, showLaunchProhibitedApps bool) map[string]interface{} { - returnAttributes := []string{ - "ApplicationDSID", - "ApplicationType", - "CFBundleDisplayName", - "CFBundleExecutable", - "CFBundleIdentifier", - "CFBundleName", - "CFBundleShortVersionString", - "CFBundleVersion", - "Container", - "Entitlements", - "EnvironmentVariables", - "MinimumOSVersion", - "Path", - "ProfileValidated", - "SBAppTags", - "SignerIdentity", - "UIDeviceFamily", - "UIRequiredDeviceCapabilities", - "UIFileSharingEnabled", - } - clientOptions := map[string]interface{}{ - "ReturnAttributes": returnAttributes, - } + clientOptions := map[string]any{} if applicationType != "" && applicationType != "Filesharing" { clientOptions["ApplicationType"] = applicationType } @@ -173,24 +175,53 @@ type BrowseResponse struct { Status string CurrentList []AppInfo } -type AppInfo struct { - ApplicationDSID int - ApplicationType string - CFBundleDisplayName string - CFBundleExecutable string - CFBundleIdentifier string - CFBundleName string - CFBundleShortVersionString string - CFBundleVersion string - Container string - Entitlements map[string]interface{} - EnvironmentVariables map[string]interface{} - MinimumOSVersion string - Path string - ProfileValidated bool - SBAppTags []string - SignerIdentity string - UIDeviceFamily []int - UIRequiredDeviceCapabilities []string - UIFileSharingEnabled bool +type AppInfo map[string]any + +func (a AppInfo) CFBundleIdentifier() string { + if bundleId, ok := a[CFBundleIdentifier].(string); ok { + return bundleId + } + return "" +} + +func (a AppInfo) Path() string { + if path, ok := a[Path].(string); ok { + return path + } + return "" +} + +func (a AppInfo) CFBundleName() string { + if bundleName, ok := a[CFBundleName].(string); ok { + return bundleName + } + return "" +} + +func (a AppInfo) EnvironmentVariables() map[string]any { + if envVars, ok := a[EnvironmentVariables].(map[string]any); ok { + return envVars + } + return make(map[string]any) +} + +func (a AppInfo) CFBundleExecutable() string { + if executable, ok := a[CFBundleExecutable].(string); ok { + return executable + } + return "" +} + +func (a AppInfo) CFBundleShortVersionString() string { + if shortVersion, ok := a[CFBundleShortVersionString].(string); ok { + return shortVersion + } + return "" +} + +func (a AppInfo) UIFileSharingEnabled() bool { + if fileSharingEnabled, ok := a[UIFileSharingEnabled].(bool); ok { + return fileSharingEnabled + } + return false } diff --git a/ios/instruments/helper.go b/ios/instruments/helper.go index 42d19d1b..fcece086 100644 --- a/ios/instruments/helper.go +++ b/ios/instruments/helper.go @@ -2,6 +2,7 @@ package instruments import ( "fmt" + "reflect" "github.com/danielpaulus/go-ios/ios" dtx "github.com/danielpaulus/go-ios/ios/dtx_codec" @@ -24,6 +25,17 @@ func (p loggingDispatcher) Dispatch(m dtx.Message) { log.Debug(m) } +func connectInstrumentsWithMsgDispatcher(device ios.DeviceEntry, dispatcher dtx.Dispatcher) (*dtx.Connection, error) { + dtxConn, err := connectInstruments(device) + if err != nil { + return nil, err + } + dtxConn.MessageDispatcher = dispatcher + log.Debugf("msg dispatcher: %v attached to instruments connection", reflect.TypeOf(dispatcher)) + + return dtxConn, nil +} + func connectInstruments(device ios.DeviceEntry) (*dtx.Connection, error) { if device.SupportsRsd() { log.Debugf("Connecting to %s", serviceNameRsd) diff --git a/ios/instruments/instruments_deviceinfo.go b/ios/instruments/instruments_deviceinfo.go index 5f9e1760..8f91aa78 100644 --- a/ios/instruments/instruments_deviceinfo.go +++ b/ios/instruments/instruments_deviceinfo.go @@ -20,6 +20,24 @@ type ProcessInfo struct { StartDate time.Time } +// processAttributes returns the attributes list which can be used for monitoring +func (d DeviceInfoService) processAttributes() ([]interface{}, error) { + resp, err := d.channel.MethodCall("sysmonProcessAttributes") + if err != nil { + return nil, err + } + return resp.Payload[0].([]interface{}), nil +} + +// systemAttributes returns the attributes list which can be used for monitoring +func (d DeviceInfoService) systemAttributes() ([]interface{}, error) { + resp, err := d.channel.MethodCall("sysmonSystemAttributes") + if err != nil { + return nil, err + } + return resp.Payload[0].([]interface{}), nil +} + // ProcessList returns a []ProcessInfo, one for each process running on the iOS device func (d DeviceInfoService) ProcessList() ([]ProcessInfo, error) { resp, err := d.channel.MethodCall("runningProcesses") diff --git a/ios/instruments/instruments_sysmontap.go b/ios/instruments/instruments_sysmontap.go new file mode 100644 index 00000000..37c883da --- /dev/null +++ b/ios/instruments/instruments_sysmontap.go @@ -0,0 +1,177 @@ +package instruments + +import ( + "fmt" + + "github.com/danielpaulus/go-ios/ios" + dtx "github.com/danielpaulus/go-ios/ios/dtx_codec" + log "github.com/sirupsen/logrus" +) + +type sysmontapMsgDispatcher struct { + messages chan dtx.Message +} + +func newSysmontapMsgDispatcher() *sysmontapMsgDispatcher { + return &sysmontapMsgDispatcher{make(chan dtx.Message)} +} + +func (p *sysmontapMsgDispatcher) Dispatch(m dtx.Message) { + p.messages <- m +} + +const sysmontapName = "com.apple.instruments.server.services.sysmontap" + +type sysmontapService struct { + channel *dtx.Channel + conn *dtx.Connection + + deviceInfoService *DeviceInfoService + msgDispatcher *sysmontapMsgDispatcher +} + +// NewSysmontapService creates a new sysmontapService +// - samplingInterval is the rate how often to get samples, i.e Xcode's default is 10, which results in sampling output +// each 1 second, with 500 the samples are retrieved every 15 seconds. It doesn't make any correlation between +// the expected rate and the actual rate of samples delivery. We can only conclude, that the lower the rate in digits, +// the faster the samples are delivered +func NewSysmontapService(device ios.DeviceEntry, samplingInterval int) (*sysmontapService, error) { + deviceInfoService, err := NewDeviceInfoService(device) + if err != nil { + return nil, err + } + + msgDispatcher := newSysmontapMsgDispatcher() + dtxConn, err := connectInstrumentsWithMsgDispatcher(device, msgDispatcher) + if err != nil { + return nil, err + } + + processControlChannel := dtxConn.RequestChannelIdentifier(sysmontapName, loggingDispatcher{dtxConn}) + + sysAttrs, err := deviceInfoService.systemAttributes() + if err != nil { + return nil, err + } + + procAttrs, err := deviceInfoService.processAttributes() + if err != nil { + return nil, err + } + + config := map[string]interface{}{ + "ur": samplingInterval, + "bm": 0, + "procAttrs": procAttrs, + "sysAttrs": sysAttrs, + "cpuUsage": true, + "physFootprint": true, + "sampleInterval": 500000000, + } + _, err = processControlChannel.MethodCall("setConfig:", config) + if err != nil { + return nil, err + } + + err = processControlChannel.MethodCallAsync("start") + if err != nil { + return nil, err + } + + return &sysmontapService{processControlChannel, dtxConn, deviceInfoService, msgDispatcher}, nil +} + +// Close closes up the DTX connection, message dispatcher and dtx.Message channel +func (s *sysmontapService) Close() error { + close(s.msgDispatcher.messages) + + s.deviceInfoService.Close() + return s.conn.Close() +} + +// ReceiveCPUUsage returns a chan of SysmontapMessage with CPU Usage info +// The method will close the result channel automatically as soon as sysmontapMsgDispatcher's +// dtx.Message channel is closed. +func (s *sysmontapService) ReceiveCPUUsage() chan SysmontapMessage { + messages := make(chan SysmontapMessage) + go func() { + defer close(messages) + + for msg := range s.msgDispatcher.messages { + sysmontapMessage, err := mapToCPUUsage(msg) + if err != nil { + log.Debugf("expected `sysmontapMessage` from global channel, but received %v", msg) + continue + } + + messages <- sysmontapMessage + } + + log.Infof("sysmontap message dispatcher channel closed") + }() + + return messages +} + +// SysmontapMessage is a wrapper struct for incoming CPU samples +type SysmontapMessage struct { + CPUCount uint64 + EnabledCPUs uint64 + EndMachAbsTime uint64 + Type uint64 + SystemCPUUsage CPUUsage +} + +type CPUUsage struct { + CPU_TotalLoad float64 +} + +func mapToCPUUsage(msg dtx.Message) (SysmontapMessage, error) { + payload := msg.Payload + if len(payload) != 1 { + return SysmontapMessage{}, fmt.Errorf("payload of message should have only one element: %+v", msg) + } + + resultArray, ok := payload[0].([]interface{}) + if !ok { + return SysmontapMessage{}, fmt.Errorf("expected resultArray of type []interface{}: %+v", payload[0]) + } + resultMap, ok := resultArray[0].(map[string]interface{}) + if !ok { + return SysmontapMessage{}, fmt.Errorf("expected resultMap of type map[string]interface{} as a single element of resultArray: %+v", resultArray[0]) + } + cpuCount, ok := resultMap["CPUCount"].(uint64) + if !ok { + return SysmontapMessage{}, fmt.Errorf("expected CPUCount of type uint64 of resultMap: %+v", resultMap) + } + enabledCPUs, ok := resultMap["EnabledCPUs"].(uint64) + if !ok { + return SysmontapMessage{}, fmt.Errorf("expected EnabledCPUs of type uint64 of resultMap: %+v", resultMap) + } + endMachAbsTime, ok := resultMap["EndMachAbsTime"].(uint64) + if !ok { + return SysmontapMessage{}, fmt.Errorf("expected EndMachAbsTime of type uint64 of resultMap: %+v", resultMap) + } + typ, ok := resultMap["Type"].(uint64) + if !ok { + return SysmontapMessage{}, fmt.Errorf("expected Type of type uint64 of resultMap: %+v", resultMap) + } + sysmontapMessageMap, ok := resultMap["SystemCPUUsage"].(map[string]interface{}) + if !ok { + return SysmontapMessage{}, fmt.Errorf("expected SystemCPUUsage of type map[string]interface{} of resultMap: %+v", resultMap) + } + cpuTotalLoad, ok := sysmontapMessageMap["CPU_TotalLoad"].(float64) + if !ok { + return SysmontapMessage{}, fmt.Errorf("expected CPU_TotalLoad of type uint64 of sysmontapMessageMap: %+v", sysmontapMessageMap) + } + cpuUsage := CPUUsage{CPU_TotalLoad: cpuTotalLoad} + + sysmontapMessage := SysmontapMessage{ + cpuCount, + enabledCPUs, + endMachAbsTime, + typ, + cpuUsage, + } + return sysmontapMessage, nil +} diff --git a/ios/instruments/processcontrol.go b/ios/instruments/processcontrol.go index 407434ef..b0118804 100644 --- a/ios/instruments/processcontrol.go +++ b/ios/instruments/processcontrol.go @@ -23,14 +23,32 @@ 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"} 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() } @@ -44,6 +62,20 @@ 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) { + aux := dtx.NewPrimitiveDictionary() + aux.AddInt32(int(pid)) + msg, err := p.processControlChannel.MethodCallWithAuxiliary("requestDisableMemoryLimitsForPid:", aux) + 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) 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 } diff --git a/ios/listdevices.go b/ios/listdevices.go index 084a506b..f5fbcb35 100755 --- a/ios/listdevices.go +++ b/ios/listdevices.go @@ -59,6 +59,7 @@ type DeviceEntry struct { Address string Rsd RsdPortProvider UserspaceTUN bool + UserspaceTUNHost string UserspaceTUNPort int } diff --git a/ios/mcinstall/prepare.go b/ios/mcinstall/prepare.go index 75226dff..ade4be30 100644 --- a/ios/mcinstall/prepare.go +++ b/ios/mcinstall/prepare.go @@ -19,11 +19,13 @@ const ( // here - https://developer.apple.com/documentation/devicemanagement/skipkeys var skipAllSetup = []string{ "Accessibility", + "ActionButton", "Android", "Appearance", "AppleID", "AppStore", "Biometric", + "CameraButton", "DeviceToDeviceMigration", "Diagnostics", "EnableLockdownMode", @@ -31,6 +33,8 @@ var skipAllSetup = []string{ "iCloudDiagnostics", "iCloudStorage", "iMessageAndFaceTime", + "Intelligence", + "Keyboard", "Location", "MessagingActivationUsingPhoneNumber", "Passcode", @@ -44,8 +48,10 @@ var skipAllSetup = []string{ "SIMSetup", "Siri", "SoftwareUpdate", + "SpokenLanguage", "TapToSetup", "TermsOfAddress", + "TOS", "TVHomeScreenSync", "TVProviderSignIn", "TVRoom", diff --git a/ios/nskeyedarchiver/archiver_test.go b/ios/nskeyedarchiver/archiver_test.go index 8e8abdc2..6c35eb02 100644 --- a/ios/nskeyedarchiver/archiver_test.go +++ b/ios/nskeyedarchiver/archiver_test.go @@ -8,6 +8,7 @@ import ( "reflect" "testing" + "github.com/Masterminds/semver" "github.com/danielpaulus/go-ios/ios/nskeyedarchiver" archiver "github.com/danielpaulus/go-ios/ios/nskeyedarchiver" "github.com/google/uuid" @@ -36,7 +37,7 @@ func TestArchiveSlice(t *testing.T) { // TODO currently only partially decoding XCTestConfig is supported, fix later func TestXCTestconfig(t *testing.T) { uuid := uuid.New() - config := nskeyedarchiver.NewXCTestConfiguration("productmodulename", uuid, "targetAppBundle", "targetAppPath", "testBundleUrl", nil, nil, false) + config := nskeyedarchiver.NewXCTestConfiguration("productmodulename", uuid, "targetAppBundle", "targetAppPath", "testBundleUrl", nil, nil, false, semver.MustParse("17.0.0")) result, err := nskeyedarchiver.ArchiveXML(config) if err != nil { log.Error(err) diff --git a/ios/nskeyedarchiver/objectivec_classes.go b/ios/nskeyedarchiver/objectivec_classes.go index ab2a4237..d29b90b1 100644 --- a/ios/nskeyedarchiver/objectivec_classes.go +++ b/ios/nskeyedarchiver/objectivec_classes.go @@ -5,6 +5,7 @@ import ( "regexp" "time" + "github.com/Masterminds/semver" "github.com/google/uuid" "howett.net/plist" ) @@ -84,6 +85,7 @@ func NewXCTestConfiguration( testsToRun []string, testsToSkip []string, isXCTest bool, + version *semver.Version, ) XCTestConfiguration { contents := map[string]interface{}{} @@ -119,7 +121,11 @@ func NewXCTestConfiguration( } contents["aggregateStatisticsBeforeCrash"] = map[string]interface{}{"XCSuiteRecordsKey": map[string]interface{}{}} - contents["automationFrameworkPath"] = "/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework" + if version.Major() >= 17 { + contents["automationFrameworkPath"] = "/System/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework" + } else { + contents["automationFrameworkPath"] = "/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework" + } contents["baselineFileRelativePath"] = plist.UID(0) contents["baselineFileURL"] = plist.UID(0) contents["defaultTestExecutionTimeAllowance"] = plist.UID(0) @@ -580,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 diff --git a/ios/springboard/client.go b/ios/springboard/client.go new file mode 100644 index 00000000..3f7842ed --- /dev/null +++ b/ios/springboard/client.go @@ -0,0 +1,108 @@ +package springboard + +import ( + "fmt" + + "github.com/danielpaulus/go-ios/ios" +) + +// Client is a connection to the `com.apple.springboardservices` service on the device +type Client struct { + connection ios.DeviceConnectionInterface + plistCodec ios.PlistCodecReadWriter +} + +func NewClient(d ios.DeviceEntry) (*Client, error) { + conn, err := ios.ConnectToService(d, "com.apple.springboardservices") + if err != nil { + return nil, fmt.Errorf("could not connect to 'com.apple.springboardservices': %w", err) + } + return &Client{ + connection: conn, + plistCodec: ios.NewPlistCodecReadWriter(conn.Reader(), conn.Writer()), + }, nil +} + +func (c *Client) Close() error { + return c.connection.Close() +} + +// ListIcons provides the homescreen layout of the device +func (c *Client) ListIcons() ([]Screen, error) { + err := c.plistCodec.Write(map[string]any{ + "command": "getIconState", + "formatVersion": "2", + }) + if err != nil { + return nil, fmt.Errorf("could not write plist: %w", err) + } + var response [][]internalUnmarshaler + err = c.plistCodec.Read(&response) + if err != nil { + return nil, fmt.Errorf("could not read plist: %w", err) + } + + screens := make([]Screen, len(response)) + for i, s := range response { + screen := make([]Icon, len(s)) + for j, t := range s { + screen[j] = t.icon + } + screens[i] = screen + } + + return screens, nil +} + +// Screen is a list of Icons displayed on one page of the home screen +// the first entry is always the bar on the bottom of the home screen (also if it is empty) +type Screen []Icon + +type Icon interface { + DisplayName() string +} + +// Folder is a collection of items in a folder. Folders can also have multiple pages +type Folder struct { + Name string `plist:"displayName"` + Icons [][]Icon + ListType string `plist:"listType"` +} + +func (f Folder) DisplayName() string { + return f.Name +} + +// AppIcon represent a native app +type AppIcon struct { + Name string `plist:"displayName"` + DisplayIdentifier string `plist:"displayIdentifier"` + BundleId string `plist:"bundleIdentifier"` + BundleVersion string `plist:"bundleVersion"` +} + +func (a AppIcon) DisplayName() string { + return a.Name +} + +// WebClip represent a Safari bookmark, or a progressive-web-app +type WebClip struct { + Name string `plist:"displayName"` + DisplayIdentifier string `plist:"displayIdentifier"` + URL string `plist:"webClipURL"` +} + +func (w WebClip) DisplayName() string { + return w.Name +} + +// Custom may be a widget or a paginated widget on the homescreen. We don't provide any information for this type +// of icon on the home screen +type Custom struct { + IconType string `plist:"iconType"` +} + +func (c Custom) DisplayName() string { + // custom icons don't have a display name + return "" +} diff --git a/ios/springboard/client_test.go b/ios/springboard/client_test.go new file mode 100644 index 00000000..ff9238c0 --- /dev/null +++ b/ios/springboard/client_test.go @@ -0,0 +1,31 @@ +//go:build !fast +// +build !fast + +package springboard + +import ( + "testing" + + "github.com/danielpaulus/go-ios/ios" + "github.com/stretchr/testify/assert" +) + +func TestListIcons(t *testing.T) { + list, err := ios.ListDevices() + assert.NoError(t, err) + if len(list.DeviceList) == 0 { + t.Skip("No devices found") + return + } + device := list.DeviceList[0] + + client, err := NewClient(device) + assert.NoError(t, err) + defer client.Close() + + screens, err := client.ListIcons() + + assert.NoError(t, err) + // As the contents are individual to each device, we can only check that something gets returned + assert.Greater(t, len(screens), 0) +} diff --git a/ios/springboard/unmarshal.go b/ios/springboard/unmarshal.go new file mode 100644 index 00000000..d380eefb --- /dev/null +++ b/ios/springboard/unmarshal.go @@ -0,0 +1,70 @@ +package springboard + +import "fmt" + +// internalUnmarshaler is a helper struct that knows how to unmarshal a plist that represents an Icon. +type internalUnmarshaler struct { + icon Icon +} + +// UnmarshalPlist will try to unmarshall the different types of Icons we have and populate the icon field +// with the detected type of Icon +func (t *internalUnmarshaler) UnmarshalPlist(unmarshal func(interface{}) error) error { + var app AppIcon + err := unmarshal(&app) + if err != nil { + return err + } + if len(app.BundleId) > 0 { + t.icon = app + return nil + } + + var webclip WebClip + err = unmarshal(&webclip) + if err != nil { + return err + } + if len(webclip.URL) > 0 { + t.icon = webclip + return nil + } + + var custom Custom + err = unmarshal(&custom) + if err != nil { + return err + } + if custom.IconType == "custom" { + t.icon = custom + return nil + } + + var list Folder + err = unmarshal(&list) + if err != nil { + return err + } + if list.ListType == "folder" { + var f folderUnmarshaler + err := unmarshal(&f) + if err != nil { + return err + } + list.Icons = make([][]Icon, len(f.Icons)) + for i, icons := range f.Icons { + list.Icons[i] = make([]Icon, len(icons)) + for j, icon := range icons { + list.Icons[i][j] = icon.icon + } + } + t.icon = list + return nil + } + + return fmt.Errorf("could not unmarshal plist") +} + +type folderUnmarshaler struct { + Icons [][]internalUnmarshaler `plist:"iconLists"` +} diff --git a/ios/startsession.go b/ios/startsession.go index 3b2aa2c0..cbd3c358 100755 --- a/ios/startsession.go +++ b/ios/startsession.go @@ -2,6 +2,7 @@ package ios import ( "bytes" + "fmt" plist "howett.net/plist" ) @@ -29,6 +30,7 @@ type StartSessionResponse struct { EnableSessionSSL bool Request string SessionID string + Error string } func startSessionResponsefromBytes(plistBytes []byte) StartSessionResponse { @@ -52,6 +54,9 @@ func (lockDownConn *LockDownConnection) StartSession(pairRecord PairRecord) (Sta return StartSessionResponse{}, err } response := startSessionResponsefromBytes(resp) + if response.Error != "" { + return StartSessionResponse{}, fmt.Errorf("failed to start new lockdown session: %s", response.Error) + } lockDownConn.sessionID = response.SessionID if response.EnableSessionSSL { err = lockDownConn.deviceConnection.EnableSessionSsl(pairRecord) diff --git a/ios/syslog/syslog.go b/ios/syslog/syslog.go index 55491415..26216ac8 100644 --- a/ios/syslog/syslog.go +++ b/ios/syslog/syslog.go @@ -2,7 +2,11 @@ package syslog import ( "bufio" + "fmt" "io" + "regexp" + "strings" + "time" "github.com/danielpaulus/go-ios/ios" ) @@ -62,6 +66,61 @@ func (sysLogConn *Connection) ReadLogMessage() (string, error) { return logmsg, nil } +// LogEntry represents a parsed log entry +type LogEntry struct { + Timestamp string `json:"timestamp"` + Device string `json:"device"` + Process string `json:"process"` + PID string `json:"pid"` + Level string `json:"level"` + Message string `json:"message"` +} + +func Parser() func(log string) (*LogEntry, error) { + pattern := `(?P[A-Z][a-z]{2} \d{1,2} \d{2}:\d{2}:\d{2}) (?P\S+) (?P[^\[]+)\[(?P\d+)\] <(?P\w+)>: (?P.+)` + regexp := regexp.MustCompile(pattern) + + return func(log string) (*LogEntry, error) { + // Match the log message against the regex pattern + match := regexp.FindStringSubmatch(log) + if match == nil { + return nil, fmt.Errorf("failed to parse syslog message: %s", log) + } + + // Create a map of named capture groups + result := make(map[string]string) + for i, name := range regexp.SubexpNames() { + if i != 0 && name != "" { + result[name] = match[i] + } + } + + // Parse the original timestamp + originalTimestamp := result["Timestamp"] + parsedTime, err := time.Parse("Jan 2 15:04:05", originalTimestamp) + // Set the year to the current year from the system (this might cause friction at year end) + parsedTime = parsedTime.AddDate(time.Now().Year()-parsedTime.Year(), 0, 0) + if err != nil { + return nil, fmt.Errorf("failed to parse syslog timestamp: %s", log) + } + + // Convert to ISO 8601 format + isoTimestamp := parsedTime.Format("2006-01-02T15:04:05") + + // Populate the LogEntry struct + entry := &LogEntry{ + Timestamp: isoTimestamp, + Device: result["Device"], + Process: strings.TrimSpace(result["Process"]), + PID: result["PID"], + Level: result["Level"], + Message: result["Message"], + } + + return entry, nil + } +} + // Close closes the underlying UsbMuxConnection func (sysLogConn *Connection) Close() error { return sysLogConn.closer.Close() diff --git a/ios/testmanagerd/proxydispatcher.go b/ios/testmanagerd/proxydispatcher.go index c8d66b22..99c243e9 100644 --- a/ios/testmanagerd/proxydispatcher.go +++ b/ios/testmanagerd/proxydispatcher.go @@ -288,6 +288,32 @@ func (p proxyDispatcher) Dispatch(m dtx.Message) { } p.testListener.testCaseDidStartForClass(testClass, testMethod) + case "_XCT_testCaseDidStartWithIdentifier:iteration:": + argumentLengthErr := assertArgumentsLengthEqual(m, 2) + if argumentLengthErr != nil { + decoderErr = argumentLengthErr + break + } + + testIdentifier, decoderErr := extractTestIdentifierArg(m, 0) + if decoderErr != nil { + break + } + + p.testListener.testCaseDidStartForClass(testIdentifier.C[0], testIdentifier.C[1]) + case "_XCT_testCaseDidStartWithIdentifier:": + argumentLengthErr := assertArgumentsLengthEqual(m, 1) + if argumentLengthErr != nil { + decoderErr = argumentLengthErr + break + } + + testIdentifier, decoderErr := extractTestIdentifierArg(m, 0) + if decoderErr != nil { + break + } + + p.testListener.testCaseDidStartForClass(testIdentifier.C[0], testIdentifier.C[1]) case "_XCT_testCaseDidStartWithIdentifier:testCaseRunConfiguration:": argumentLengthErr := assertArgumentsLengthEqual(m, 2) if argumentLengthErr != nil { diff --git a/ios/testmanagerd/testdata/contains_invalid_test_configuration.xctestrun b/ios/testmanagerd/testdata/contains_invalid_test_configuration.xctestrun new file mode 100644 index 00000000..09f9764f --- /dev/null +++ b/ios/testmanagerd/testdata/contains_invalid_test_configuration.xctestrun @@ -0,0 +1,11 @@ + + + + + __xctestrun_metadata__ + + FormatVersion + 2 + + + \ No newline at end of file diff --git a/ios/testmanagerd/testdata/format_version_1.xctestrun b/ios/testmanagerd/testdata/format_version_1.xctestrun new file mode 100644 index 00000000..a725e9b3 --- /dev/null +++ b/ios/testmanagerd/testdata/format_version_1.xctestrun @@ -0,0 +1,103 @@ + + + + + RunnerTests + + BlueprintName + RunnerTests + BlueprintProviderName + Runner + BlueprintProviderRelativePath + Runner.xcodeproj + BundleIdentifiersForCrashReportEmphasis + + com.example.myApp + com.example.myApp.RunnerTests + + CommandLineArguments + + DefaultTestExecutionTimeAllowance + 600 + DependentProductPaths + + __TESTROOT__/Release-iphoneos/Runner.app + __TESTROOT__/Release-iphoneos/Runner.app/PlugIns/RunnerTests.xctest + + DiagnosticCollectionPolicy + 1 + EnvironmentVariables + + APP_DISTRIBUTOR_ID_OVERRIDE + com.apple.AppStore + OS_ACTIVITY_DT_MODE + YES + SQLITE_ENABLE_THREAD_ASSERTIONS + 1 + TERM + dumb + + IsAppHostedTestBundle + + ParallelizationEnabled + + PreferredScreenCaptureFormat + screenRecording + ProductModuleName + RunnerTests + RunOrder + 0 + SystemAttachmentLifetime + deleteOnSuccess + TestBundlePath + __TESTHOST__/PlugIns/RunnerTests.xctest + TestHostBundleIdentifier + com.example.myApp + TestHostPath + __TESTROOT__/Release-iphoneos/Runner.app + TestLanguage + + TestRegion + + TestTimeoutsEnabled + + TestingEnvironmentVariables + + DYLD_INSERT_LIBRARIES + __TESTHOST__/Frameworks/libXCTestBundleInject.dylib + XCInjectBundleInto + unused + Test + xyz + + ToolchainsSettingValue + + UserAttachmentLifetime + deleteOnSuccess + OnlyTestIdentifiers + + TestClass1/testMethod1 + TestClass2/testMethod1 + + SkipTestIdentifiers + + TestClass1/testMethod2 + TestClass2/testMethod2 + + IsUITestBundle + + + __xctestrun_metadata__ + + ContainerInfo + + ContainerName + Runner + SchemeName + Runner + + FormatVersion + 1 + + + \ No newline at end of file diff --git a/ios/testmanagerd/testdata/format_version_2.xctestrun b/ios/testmanagerd/testdata/format_version_2.xctestrun new file mode 100644 index 00000000..90eaa24f --- /dev/null +++ b/ios/testmanagerd/testdata/format_version_2.xctestrun @@ -0,0 +1,278 @@ + + + + + CodeCoverageBuildableInfos + + + Architectures + + arm64 + + BuildableIdentifier + 506AF5D12D429D9E008E829B:primary + IncludeInReport + + IsStatic + + Name + FakeCounterApp.app + ProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app/FakeCounterApp + + SourceFiles + + CounterView.swift + CounterViewModel.swift + FakeCounterApp.swift + + SourceFilesCommonPathPrefix + /Users/mootazbahri/Desktop/apps/rdc-xcuitest-test/app_sources/FakeCounterApp/FakeCounterApp/ + Toolchains + + com.apple.dt.toolchain.XcodeDefault + + + + Architectures + + arm64 + + BuildableIdentifier + 506AF5E12D429DA0008E829B:primary + IncludeInReport + + IsStatic + + Name + FakeCounterAppTests.xctest + ProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app/PlugIns/FakeCounterAppTests.xctest/FakeCounterAppTests + + SourceFiles + + CounterXCTests.swift + SkippedTests.swift + + SourceFilesCommonPathPrefix + /Users/mootazbahri/Desktop/apps/rdc-xcuitest-test/app_sources/FakeCounterApp/FakeCounterAppXCTests/ + Toolchains + + com.apple.dt.toolchain.XcodeDefault + + + + Architectures + + arm64 + + BuildableIdentifier + 506AF5EB2D429DA0008E829B:primary + IncludeInReport + + IsStatic + + Name + FakeCounterAppUITests.xctest + ProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app/PlugIns/FakeCounterAppUITests.xctest/FakeCounterAppUITests + + SourceFiles + + /Users/mootazbahri/Desktop/apps/rdc-xcuitest-test/app_sources/FakeCounterApp/FakeCounterAppUITests/CounterUITests.swift + + Toolchains + + com.apple.dt.toolchain.XcodeDefault + + + + ContainerInfo + + ContainerName + FakeCounterApp + SchemeName + FakeCounterAppTest + + TestConfigurations + + + Name + Test Scheme Action + TestTargets + + + BlueprintName + FakeCounterAppTests + BlueprintProviderName + FakeCounterApp + BlueprintProviderRelativePath + FakeCounterApp.xcodeproj + BundleIdentifiersForCrashReportEmphasis + + saucelabs.FakeCounterApp + saucelabs.FakeCounterAppUITests + + ClangProfileDataDirectoryPath + __DERIVEDDATA__/Build/ProfileData + CommandLineArguments + + DefaultTestExecutionTimeAllowance + 600 + DependentProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app/PlugIns/FakeCounterAppTests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app/PlugIns/FakeCounterAppUITests.xctest + + DiagnosticCollectionPolicy + 1 + EnvironmentVariables + + APP_DISTRIBUTOR_ID_OVERRIDE + com.apple.AppStore + OS_ACTIVITY_DT_MODE + YES + SQLITE_ENABLE_THREAD_ASSERTIONS + 1 + TERM + dumb + + IsAppHostedTestBundle + + ParallelizationEnabled + + PreferredScreenCaptureFormat + screenRecording + ProductModuleName + FakeCounterAppTests + SkipTestIdentifiers + + SkippedTests + SkippedTests/testThatAlwaysFailsAndShouldBeSkipped + + SystemAttachmentLifetime + deleteOnSuccess + TestBundlePath + __TESTHOST__/PlugIns/FakeCounterAppTests.xctest + TestHostBundleIdentifier + saucelabs.FakeCounterApp + TestHostPath + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app + TestLanguage + + TestRegion + + TestTimeoutsEnabled + + TestingEnvironmentVariables + + DYLD_INSERT_LIBRARIES + __TESTHOST__/Frameworks/libXCTestBundleInject.dylib + XCInjectBundleInto + unused + + ToolchainsSettingValue + + UserAttachmentLifetime + deleteOnSuccess + + + BlueprintName + FakeCounterAppUITests + BlueprintProviderName + FakeCounterApp + BlueprintProviderRelativePath + FakeCounterApp.xcodeproj + BundleIdentifiersForCrashReportEmphasis + + saucelabs.FakeCounterApp + saucelabs.FakeCounterAppUITests + + ClangProfileDataDirectoryPath + __DERIVEDDATA__/Build/ProfileData + CommandLineArguments + + DefaultTestExecutionTimeAllowance + 600 + DependentProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app/PlugIns/FakeCounterAppTests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app/PlugIns/FakeCounterAppUITests.xctest + + DiagnosticCollectionPolicy + 1 + EnvironmentVariables + + APP_DISTRIBUTOR_ID_OVERRIDE + com.apple.AppStore + OS_ACTIVITY_DT_MODE + YES + SQLITE_ENABLE_THREAD_ASSERTIONS + 1 + TERM + dumb + + IsUITestBundle + + IsXCTRunnerHostedTestBundle + + ParallelizationEnabled + + PreferredScreenCaptureFormat + screenRecording + ProductModuleName + FakeCounterAppUITests + SystemAttachmentLifetime + deleteOnSuccess + TestBundlePath + __TESTHOST__/PlugIns/FakeCounterAppUITests.xctest + TestHostBundleIdentifier + saucelabs.FakeCounterAppUITests.xctrunner + TestHostPath + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app + TestLanguage + + TestRegion + + TestTimeoutsEnabled + + TestingEnvironmentVariables + + ToolchainsSettingValue + + UITargetAppCommandLineArguments + + UITargetAppEnvironmentVariables + + APP_DISTRIBUTOR_ID_OVERRIDE + com.apple.AppStore + + UITargetAppPath + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app + UserAttachmentLifetime + deleteOnSuccess + + + + + TestPlan + + IsDefault + + Name + FakeAppTestPlan + + __xctestrun_metadata__ + + FormatVersion + 2 + + + \ No newline at end of file diff --git a/ios/testmanagerd/testdata/format_version_2_multiple.xctestrun b/ios/testmanagerd/testdata/format_version_2_multiple.xctestrun new file mode 100644 index 00000000..4af922b6 --- /dev/null +++ b/ios/testmanagerd/testdata/format_version_2_multiple.xctestrun @@ -0,0 +1,579 @@ + + + + + CodeCoverageBuildableInfos + + + Architectures + + arm64 + + BuildableIdentifier + 506AF5D12D429D9E008E829B:primary + IncludeInReport + + IsStatic + + Name + FakeCounterApp.app + ProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app/FakeCounterApp + + SourceFiles + + CounterView.swift + CounterViewModel.swift + FakeCounterApp.swift + + SourceFilesCommonPathPrefix + /Users/mootazbahri/Desktop/apps/rdc-xcuitest-test/app_sources/FakeCounterApp/FakeCounterApp/ + Toolchains + + com.apple.dt.toolchain.XcodeDefault + + + + Architectures + + arm64 + + BuildableIdentifier + 506AF5E12D429DA0008E829B:primary + IncludeInReport + + IsStatic + + Name + FakeCounterAppTests.xctest + ProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app/PlugIns/FakeCounterAppTests.xctest/FakeCounterAppTests + + SourceFiles + + CounterXCTests.swift + SkippedTests.swift + + SourceFilesCommonPathPrefix + /Users/mootazbahri/Desktop/apps/rdc-xcuitest-test/app_sources/FakeCounterApp/FakeCounterAppXCTests/ + Toolchains + + com.apple.dt.toolchain.XcodeDefault + + + + Architectures + + arm64 + + BuildableIdentifier + 506AF5EB2D429DA0008E829B:primary + IncludeInReport + + IsStatic + + Name + FakeCounterAppUITests.xctest + ProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app/PlugIns/FakeCounterAppUITests.xctest/FakeCounterAppUITests + + SourceFiles + + /Users/mootazbahri/Desktop/apps/rdc-xcuitest-test/app_sources/FakeCounterApp/FakeCounterAppUITests/CounterUITests.swift + + Toolchains + + com.apple.dt.toolchain.XcodeDefault + + + + Architectures + + arm64 + + BuildableIdentifier + 0B27D3692D562B7500514D89:primary + IncludeInReport + + IsStatic + + Name + FakeCounterDuplicateApp.app + ProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app/FakeCounterDuplicateApp + + SourceFiles + + CounterView.swift + CounterViewModel.swift + FakeCounterApp.swift + + SourceFilesCommonPathPrefix + /Users/mootazbahri/Desktop/apps/rdc-xcuitest-test/app_sources/FakeCounterApp/FakeCounterDuplicateApp/Duplicate + Toolchains + + com.apple.dt.toolchain.XcodeDefault + + + + Architectures + + arm64 + + BuildableIdentifier + 0B27D37D2D562B7700514D89:primary + IncludeInReport + + IsStatic + + Name + FakeCounterDuplicateAppTests.xctest + ProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app/PlugIns/FakeCounterDuplicateAppTests.xctest/FakeCounterDuplicateAppTests + + SourceFiles + + /Users/mootazbahri/Desktop/apps/rdc-xcuitest-test/app_sources/FakeCounterApp/FakeCounterDuplicateAppTests/FakeCounterDuplicateAppTests.swift + + Toolchains + + com.apple.dt.toolchain.XcodeDefault + + + + Architectures + + arm64 + + BuildableIdentifier + 0B27D3872D562B7700514D89:primary + IncludeInReport + + IsStatic + + Name + FakeCounterDuplicateAppUITests.xctest + ProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateAppUITests-Runner.app/PlugIns/FakeCounterDuplicateAppUITests.xctest/FakeCounterDuplicateAppUITests + + SourceFiles + + /Users/mootazbahri/Desktop/apps/rdc-xcuitest-test/app_sources/FakeCounterApp/FakeCounterDuplicateAppUITests/FakeCounterDuplicateAppUITests.swift + + Toolchains + + com.apple.dt.toolchain.XcodeDefault + + + + Architectures + + arm64 + + BuildableIdentifier + 0B19113F2D5C9B0900D7A67B:primary + IncludeInReport + + IsStatic + + Name + NoTestTargetAppUITests.xctest + ProductPaths + + __TESTROOT__/Debug-iphoneos/NoTestTargetAppUITests-Runner.app/PlugIns/NoTestTargetAppUITests.xctest/NoTestTargetAppUITests + + SourceFiles + + /Users/mootazbahri/Desktop/apps/rdc-xcuitest-test/app_sources/FakeCounterApp/NoTestTargetAppUITests/NoTestTargetAppUITests.swift + + Toolchains + + com.apple.dt.toolchain.XcodeDefault + + + + ContainerInfo + + ContainerName + FakeCounterApp + SchemeName + FakeCounterAppTest + + TestConfigurations + + + Name + TestCounterApp_1 + TestTargets + + + BlueprintName + FakeCounterAppUITests + BlueprintProviderName + FakeCounterApp + BlueprintProviderRelativePath + FakeCounterApp.xcodeproj + BundleIdentifiersForCrashReportEmphasis + + saucelabs.FakeCounterApp + saucelabs.FakeCounterAppUITests + saucelabs.FakeCounterDuplicateApp + saucelabs.FakeCounterDuplicateAppTests + saucelabs.FakeCounterDuplicateAppUITests + saucelabs.NoTestTargetAppUITests + + ClangProfileDataDirectoryPath + __DERIVEDDATA__/Build/ProfileData + CommandLineArguments + + DefaultTestExecutionTimeAllowance + 600 + DependentProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app/PlugIns/FakeCounterAppTests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app/PlugIns/FakeCounterAppUITests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app/PlugIns/FakeCounterDuplicateAppTests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateAppUITests-Runner.app/PlugIns/FakeCounterDuplicateAppUITests.xctest + __TESTROOT__/Debug-iphoneos/NoTestTargetAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/NoTestTargetAppUITests-Runner.app/PlugIns/NoTestTargetAppUITests.xctest + + DiagnosticCollectionPolicy + 1 + EnvironmentVariables + + APP_DISTRIBUTOR_ID_OVERRIDE + com.apple.AppStore + OS_ACTIVITY_DT_MODE + YES + SQLITE_ENABLE_THREAD_ASSERTIONS + 1 + TERM + dumb + + IsUITestBundle + + IsXCTRunnerHostedTestBundle + + ParallelizationEnabled + + PreferredScreenCaptureFormat + screenRecording + ProductModuleName + FakeCounterAppUITests + SystemAttachmentLifetime + deleteOnSuccess + TestBundlePath + __TESTHOST__/PlugIns/FakeCounterAppUITests.xctest + TestHostBundleIdentifier + saucelabs.FakeCounterAppUITests.xctrunner + TestHostPath + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app + TestLanguage + + TestRegion + + TestTimeoutsEnabled + + TestingEnvironmentVariables + + ToolchainsSettingValue + + UITargetAppCommandLineArguments + + UITargetAppEnvironmentVariables + + APP_DISTRIBUTOR_ID_OVERRIDE + com.apple.AppStore + + UITargetAppPath + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app + UserAttachmentLifetime + deleteOnSuccess + + + BlueprintName + FakeCounterAppTests + BlueprintProviderName + FakeCounterApp + BlueprintProviderRelativePath + FakeCounterApp.xcodeproj + BundleIdentifiersForCrashReportEmphasis + + saucelabs.FakeCounterApp + saucelabs.FakeCounterAppUITests + saucelabs.FakeCounterDuplicateApp + saucelabs.FakeCounterDuplicateAppTests + saucelabs.FakeCounterDuplicateAppUITests + saucelabs.NoTestTargetAppUITests + + ClangProfileDataDirectoryPath + __DERIVEDDATA__/Build/ProfileData + CommandLineArguments + + DefaultTestExecutionTimeAllowance + 600 + DependentProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app/PlugIns/FakeCounterAppTests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app/PlugIns/FakeCounterAppUITests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app/PlugIns/FakeCounterDuplicateAppTests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateAppUITests-Runner.app/PlugIns/FakeCounterDuplicateAppUITests.xctest + __TESTROOT__/Debug-iphoneos/NoTestTargetAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/NoTestTargetAppUITests-Runner.app/PlugIns/NoTestTargetAppUITests.xctest + + DiagnosticCollectionPolicy + 1 + EnvironmentVariables + + APP_DISTRIBUTOR_ID_OVERRIDE + com.apple.AppStore + OS_ACTIVITY_DT_MODE + YES + SQLITE_ENABLE_THREAD_ASSERTIONS + 1 + TERM + dumb + + IsAppHostedTestBundle + + ParallelizationEnabled + + PreferredScreenCaptureFormat + screenRecording + ProductModuleName + FakeCounterAppTests + SkipTestIdentifiers + + SkippedTests + SkippedTests/testThatAlwaysFailsAndShouldBeSkipped + + SystemAttachmentLifetime + deleteOnSuccess + TestBundlePath + __TESTHOST__/PlugIns/FakeCounterAppTests.xctest + TestHostBundleIdentifier + saucelabs.FakeCounterApp + TestHostPath + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app + TestLanguage + + TestRegion + + TestTimeoutsEnabled + + TestingEnvironmentVariables + + DYLD_INSERT_LIBRARIES + __TESTHOST__/Frameworks/libXCTestBundleInject.dylib + XCInjectBundleInto + unused + + ToolchainsSettingValue + + UserAttachmentLifetime + deleteOnSuccess + + + + + Name + TestDuplicateApp_2 + TestTargets + + + BlueprintName + FakeCounterDuplicateAppTests + BlueprintProviderName + FakeCounterApp + BlueprintProviderRelativePath + FakeCounterApp.xcodeproj + BundleIdentifiersForCrashReportEmphasis + + saucelabs.FakeCounterApp + saucelabs.FakeCounterAppUITests + saucelabs.FakeCounterDuplicateApp + saucelabs.FakeCounterDuplicateAppTests + saucelabs.FakeCounterDuplicateAppUITests + saucelabs.NoTestTargetAppUITests + + ClangProfileDataDirectoryPath + __DERIVEDDATA__/Build/ProfileData + CommandLineArguments + + DefaultTestExecutionTimeAllowance + 600 + DependentProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app/PlugIns/FakeCounterAppTests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app/PlugIns/FakeCounterAppUITests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app/PlugIns/FakeCounterDuplicateAppTests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateAppUITests-Runner.app/PlugIns/FakeCounterDuplicateAppUITests.xctest + __TESTROOT__/Debug-iphoneos/NoTestTargetAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/NoTestTargetAppUITests-Runner.app/PlugIns/NoTestTargetAppUITests.xctest + + DiagnosticCollectionPolicy + 1 + EnvironmentVariables + + APP_DISTRIBUTOR_ID_OVERRIDE + com.apple.AppStore + OS_ACTIVITY_DT_MODE + YES + SQLITE_ENABLE_THREAD_ASSERTIONS + 1 + TERM + dumb + + IsAppHostedTestBundle + + PreferredScreenCaptureFormat + screenRecording + ProductModuleName + FakeCounterDuplicateAppTests + SystemAttachmentLifetime + deleteOnSuccess + TestBundlePath + __TESTHOST__/PlugIns/FakeCounterDuplicateAppTests.xctest + TestHostBundleIdentifier + saucelabs.FakeCounterDuplicateApp + TestHostPath + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app + TestLanguage + + TestRegion + + TestTimeoutsEnabled + + TestingEnvironmentVariables + + DYLD_INSERT_LIBRARIES + __TESTHOST__/Frameworks/libXCTestBundleInject.dylib + XCInjectBundleInto + unused + + ToolchainsSettingValue + + UserAttachmentLifetime + deleteOnSuccess + + + BlueprintName + FakeCounterDuplicateAppUITests + BlueprintProviderName + FakeCounterApp + BlueprintProviderRelativePath + FakeCounterApp.xcodeproj + BundleIdentifiersForCrashReportEmphasis + + saucelabs.FakeCounterApp + saucelabs.FakeCounterAppUITests + saucelabs.FakeCounterDuplicateApp + saucelabs.FakeCounterDuplicateAppTests + saucelabs.FakeCounterDuplicateAppUITests + saucelabs.NoTestTargetAppUITests + + ClangProfileDataDirectoryPath + __DERIVEDDATA__/Build/ProfileData + CommandLineArguments + + DefaultTestExecutionTimeAllowance + 600 + DependentProductPaths + + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app + __TESTROOT__/Debug-iphoneos/FakeCounterApp.app/PlugIns/FakeCounterAppTests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/FakeCounterAppUITests-Runner.app/PlugIns/FakeCounterAppUITests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app/PlugIns/FakeCounterDuplicateAppTests.xctest + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateAppUITests-Runner.app/PlugIns/FakeCounterDuplicateAppUITests.xctest + __TESTROOT__/Debug-iphoneos/NoTestTargetAppUITests-Runner.app + __TESTROOT__/Debug-iphoneos/NoTestTargetAppUITests-Runner.app/PlugIns/NoTestTargetAppUITests.xctest + + DiagnosticCollectionPolicy + 1 + EnvironmentVariables + + APP_DISTRIBUTOR_ID_OVERRIDE + com.apple.AppStore + OS_ACTIVITY_DT_MODE + YES + SQLITE_ENABLE_THREAD_ASSERTIONS + 1 + TERM + dumb + + IsUITestBundle + + IsXCTRunnerHostedTestBundle + + PreferredScreenCaptureFormat + screenRecording + ProductModuleName + FakeCounterDuplicateAppUITests + SystemAttachmentLifetime + deleteOnSuccess + TestBundlePath + __TESTHOST__/PlugIns/FakeCounterDuplicateAppUITests.xctest + TestHostBundleIdentifier + saucelabs.FakeCounterDuplicateAppUITests.xctrunner + TestHostPath + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateAppUITests-Runner.app + TestLanguage + + TestRegion + + TestTimeoutsEnabled + + TestingEnvironmentVariables + + ToolchainsSettingValue + + UITargetAppCommandLineArguments + + UITargetAppEnvironmentVariables + + APP_DISTRIBUTOR_ID_OVERRIDE + com.apple.AppStore + + UITargetAppPath + __TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app + UserAttachmentLifetime + deleteOnSuccess + + + + + TestPlan + + IsDefault + + Name + FakeAppTestPlan + + __xctestrun_metadata__ + + FormatVersion + 2 + + + diff --git a/ios/testmanagerd/testlistener.go b/ios/testmanagerd/testlistener.go index 502d3738..566cfbde 100644 --- a/ios/testmanagerd/testlistener.go +++ b/ios/testmanagerd/testlistener.go @@ -164,7 +164,31 @@ func (t *TestListener) testSuiteDidStart(suiteName string, date string) { } func (t *TestListener) testCaseDidStartForClass(testClass string, testMethod string) { + // Find the existing test suite or create a new one if not found ts := t.findTestSuite(testClass) + if ts == nil { + // If no test suite is found and we're not in a running test suite, + // we should use the runningTestSuite instead of creating a new one. + // This handles cases where testCaseDidStartForClass is called before + // testSuiteDidStart, which would otherwise result in a nil pointer dereference. + + if t.runningTestSuite != nil { + ts = t.runningTestSuite + } else { + // Create a new test suite for this class if no running suite exists. + // We initialize TestCases as an empty slice to avoid potential issues with nil slices. + d := time.Now() + newSuite := TestSuite{ + Name: testClass, + StartDate: d, + TestCases: []TestCase{}, + } + t.TestSuites = append(t.TestSuites, newSuite) + ts = &t.TestSuites[len(t.TestSuites)-1] + } + } + + // Add the test case to the suite ts.TestCases = append(ts.TestCases, TestCase{ ClassName: testClass, MethodName: testMethod, @@ -256,6 +280,10 @@ func (t *TestListener) TestRunnerKilled() { } func (t *TestListener) FinishWithError(err error) { + if t.runningTestSuite != nil { + t.TestSuites = append(t.TestSuites, *t.runningTestSuite) + t.runningTestSuite = nil + } t.err = err t.executionFinished() } @@ -265,12 +293,22 @@ func (t *TestListener) Done() <-chan struct{} { } func (t *TestListener) findTestCase(className string, methodName string) *TestCase { - ts := t.findTestSuite(className) + if ts := t.findTestSuite(className); ts != nil { + if len(ts.TestCases) > 0 { + tc := &ts.TestCases[len(ts.TestCases)-1] + if tc.ClassName == className && tc.MethodName == methodName { + return tc + } + } + } - if ts != nil && len(ts.TestCases) > 0 { - tc := &ts.TestCases[len(ts.TestCases)-1] - if tc.ClassName == className && tc.MethodName == methodName { - return tc + if t.runningTestSuite != nil { + // Search backwards to find the most recent matching test case without status + for i := len(t.runningTestSuite.TestCases) - 1; i >= 0; i-- { + tc := &t.runningTestSuite.TestCases[i] + if tc.ClassName == className && tc.MethodName == methodName && tc.Status == "" { + return tc + } } } @@ -278,8 +316,10 @@ func (t *TestListener) findTestCase(className string, methodName string) *TestCa } func (t *TestListener) findTestSuite(className string) *TestSuite { - if t.runningTestSuite != nil && t.runningTestSuite.Name == className { - return t.runningTestSuite + if t.runningTestSuite != nil { + if t.runningTestSuite.Name == className { + return t.runningTestSuite + } } return nil @@ -290,3 +330,20 @@ func (t *TestListener) executionFinished() { close(t.finished) }) } + +func (t *TestListener) reset() { + // Reinitialize finished channel to allow signaling again + t.finished = make(chan struct{}) + + // Reset the sync.Once instance so it can be used again + t.finishedOnce = sync.Once{} + + // Clear error from the previous test run + t.err = nil + + // Reset test results + t.TestSuites = nil + + // Clear the reference to the running test suite + t.runningTestSuite = nil +} diff --git a/ios/testmanagerd/testlistener_test.go b/ios/testmanagerd/testlistener_test.go index ead39ba1..7c3b6749 100644 --- a/ios/testmanagerd/testlistener_test.go +++ b/ios/testmanagerd/testlistener_test.go @@ -1,6 +1,7 @@ package testmanagerd import ( + "errors" "io" "os" "sync" @@ -148,6 +149,18 @@ func TestFinishExecutingTestPlan(t *testing.T) { testListener.testCaseDidStartForClass("mysuite", "mymethod") testListener.testCaseDidFinishForTest("mysuite", "mymethod", "passed", 1.0) + t.Run("Check running test suite is saved on FinishWithError", func(t *testing.T) { + testListener := NewTestListener(io.Discard, io.Discard, os.TempDir()) + + testListener.testSuiteDidStart("mysuite", "2024-01-16 15:36:43 +0000") + testListener.testCaseDidStartForClass("mysuite", "mymethod") + testListener.FinishWithError(errors.New("test error")) + + assert.Equal(t, 1, len(testListener.TestSuites)) + assert.Equal(t, "mysuite", testListener.TestSuites[0].Name) + assert.Equal(t, 1, len(testListener.TestSuites[0].TestCases)) + }) + assert.Equal(t, 1, len(testListener.runningTestSuite.TestCases), "TestCase must be appended to list of test cases") assert.Equal(t, TestCaseStatus("passed"), testListener.runningTestSuite.TestCases[0].Status) assert.Equal(t, 1.0, testListener.runningTestSuite.TestCases[0].Duration.Seconds()) @@ -226,6 +239,26 @@ func TestFinishExecutingTestPlan(t *testing.T) { assert.Equal(t, "test", string(attachment), "Attachment content should be put in a file") }) + + t.Run("Check test case without suite initialization", func(t *testing.T) { + testListener := NewTestListener(io.Discard, io.Discard, os.TempDir()) + + // This should trigger the nil pointer dereference error if not handled properly + // Call testCaseDidStartForClass without first calling testSuiteDidStart + assert.NotPanics(t, func() { + testListener.testCaseDidStartForClass("mysuite", "mymethod") + }) + + // Verify that a new suite was created automatically + assert.Equal(t, 1, len(testListener.TestSuites), "A test suite should be created automatically") + + // Verify the test case was added to the newly created suite + assert.Equal(t, 1, len(testListener.TestSuites[0].TestCases), "TestCase must be appended to list of test cases") + assert.Equal(t, TestCase{ + ClassName: "mysuite", + MethodName: "mymethod", + }, testListener.TestSuites[0].TestCases[0]) + }) } type assertionWriter struct { @@ -239,3 +272,104 @@ func (w *assertionWriter) Write(p []byte) (n int, err error) { return len(p), nil } + +func TestFindTestSuite(t *testing.T) { + t.Run("finds suite by exact name", func(t *testing.T) { + listener := NewTestListener(io.Discard, io.Discard, os.TempDir()) + listener.testSuiteDidStart("ShowAlert", "2024-01-16 15:36:43 +0000") + + suite := listener.findTestSuite("ShowAlert") + assert.NotNil(t, suite) + assert.Equal(t, "ShowAlert", suite.Name) + }) + + t.Run("returns nil for class names that don't match suite", func(t *testing.T) { + listener := NewTestListener(io.Discard, io.Discard, os.TempDir()) + listener.testSuiteDidStart("ShowAlert", "2024-01-16 15:36:43 +0000") + + suite := listener.findTestSuite("HelloButton|ShowAlert") + assert.Nil(t, suite) + }) + + t.Run("returns nil for unrelated test names", func(t *testing.T) { + listener := NewTestListener(io.Discard, io.Discard, os.TempDir()) + listener.testSuiteDidStart("ShowAlert", "2024-01-16 15:36:43 +0000") + + suite := listener.findTestSuite("SomeOtherTest") + assert.Nil(t, suite) + }) + + t.Run("returns nil when no suite is running", func(t *testing.T) { + listener := NewTestListener(io.Discard, io.Discard, os.TempDir()) + + suite := listener.findTestSuite("ShowAlert") + assert.Nil(t, suite) + }) +} + +func TestTestCaseLookup(t *testing.T) { + t.Run("finds test case in different class within same suite", func(t *testing.T) { + listener := NewTestListener(io.Discard, io.Discard, os.TempDir()) + listener.testSuiteDidStart("ShowAlert", "2024-01-16 15:36:43 +0000") + listener.testCaseDidStartForClass("HelloButton|ShowAlert", "GivenILaunchTheApp") + + listener.testCaseDidFinishForTest("HelloButton|ShowAlert", "GivenILaunchTheApp", "passed", 1.0) + + testCase := listener.runningTestSuite.TestCases[0] + assert.Equal(t, TestCaseStatus("passed"), testCase.Status) + assert.Equal(t, 1.0, testCase.Duration.Seconds()) + }) + + t.Run("handles BDD scenario tests", func(t *testing.T) { + listener := NewTestListener(io.Discard, io.Discard, os.TempDir()) + listener.testSuiteDidStart("ShowAlert", "2024-01-16 15:36:43 +0000") + + listener.testCaseDidStartForClass("HelloButton|ShowAlert", "GivenILaunchTheApp") + listener.testCaseDidStartForClass("HelloButton|ShowAlert", "WhenITapTheHelloButton") + listener.testCaseDidStartForClass("HelloButton|ShowAlert", "ThenISeeHelloWorldAlert") + + listener.testCaseDidFinishForTest("HelloButton|ShowAlert", "GivenILaunchTheApp", "passed", 1.0) + listener.testCaseDidFinishForTest("HelloButton|ShowAlert", "WhenITapTheHelloButton", "passed", 2.0) + listener.testCaseDidFinishForTest("HelloButton|ShowAlert", "ThenISeeHelloWorldAlert", "passed", 3.0) + + cases := listener.runningTestSuite.TestCases + assert.Len(t, cases, 3) + for _, testCase := range cases { + assert.Equal(t, TestCaseStatus("passed"), testCase.Status) + assert.Greater(t, testCase.Duration.Seconds(), 0.0) + } + }) + + t.Run("works with unrelated class names in suite", func(t *testing.T) { + listener := NewTestListener(io.Discard, io.Discard, os.TempDir()) + listener.testSuiteDidStart("WidgetExpanderTest", "2024-01-16 15:36:43 +0000") + listener.testCaseDidStartForClass("MagicTest", "verifyBlablabla") + + listener.testCaseDidFinishForTest("MagicTest", "verifyBlablabla", "passed", 1.0) + + testCase := listener.runningTestSuite.TestCases[0] + assert.Equal(t, TestCaseStatus("passed"), testCase.Status) + assert.Equal(t, 1.0, testCase.Duration.Seconds()) + }) + + t.Run("handles repeated test executions", func(t *testing.T) { + listener := NewTestListener(io.Discard, io.Discard, os.TempDir()) + listener.testSuiteDidStart("WidgetExpanderTest", "2024-01-16 15:36:43 +0000") + + listener.testCaseDidStartForClass("MagicTest", "verifyBlablabla") + listener.testCaseDidStartForClass("MagicTest", "verifyBlablabla") + listener.testCaseDidStartForClass("MagicTest", "verifyBlablabla") + + listener.testCaseDidFinishForTest("MagicTest", "verifyBlablabla", "passed", 1.0) + listener.testCaseDidFinishForTest("MagicTest", "verifyBlablabla", "passed", 2.0) + listener.testCaseDidFinishForTest("MagicTest", "verifyBlablabla", "failed", 3.0) + + cases := listener.runningTestSuite.TestCases + assert.Len(t, cases, 3) + + for _, testCase := range cases { + assert.NotEqual(t, TestCaseStatus(""), testCase.Status) + assert.Greater(t, testCase.Duration.Seconds(), 0.0) + } + }) +} diff --git a/ios/testmanagerd/xctestrunutils.go b/ios/testmanagerd/xctestrunutils.go new file mode 100755 index 00000000..13a005c6 --- /dev/null +++ b/ios/testmanagerd/xctestrunutils.go @@ -0,0 +1,207 @@ +package testmanagerd + +import ( + "bytes" + "fmt" + "io" + "maps" + "os" + "path/filepath" + "strings" + + "github.com/danielpaulus/go-ios/ios" + "github.com/danielpaulus/go-ios/ios/installationproxy" + "howett.net/plist" +) + +// xctestrunutils provides utilities for parsing `.xctestrun` files. +// It simplifies the extraction of test configurations and metadata into structured objects (`xCTestRunData`), +// enabling efficient setup for iOS test execution. +// +// Features: +// - Parses `.xctestrun` files to extract test metadata and configurations. +// - Supports building `TestConfig` objects for test execution. + +// schemeData represents the structure of a scheme-specific test configuration +type schemeData struct { + TestHostBundleIdentifier string + TestBundlePath string + SkipTestIdentifiers []string + OnlyTestIdentifiers []string + IsUITestBundle bool + CommandLineArguments []string + EnvironmentVariables map[string]any + TestingEnvironmentVariables map[string]any + UITargetAppEnvironmentVariables map[string]any + UITargetAppPath string +} + +type testConfiguration struct { + Name string `plist:"Name"` + TestTargets []schemeData `plist:"TestTargets"` +} + +func (data schemeData) buildTestConfig(device ios.DeviceEntry, listener *TestListener, installedApps []installationproxy.AppInfo) (TestConfig, error) { + testsToRun := data.OnlyTestIdentifiers + testsToSkip := data.SkipTestIdentifiers + + testEnv := make(map[string]any) + var bundleId string + + if data.IsUITestBundle { + maps.Copy(testEnv, data.EnvironmentVariables) + maps.Copy(testEnv, data.TestingEnvironmentVariables) + maps.Copy(testEnv, data.UITargetAppEnvironmentVariables) + // Only call getBundleID if : + // - allAps is provided + // - UITargetAppPath is populated since it can be empty for UI tests in some edge cases + if len(data.UITargetAppPath) > 0 && installedApps != nil { + bundleId = getBundleId(installedApps, data.UITargetAppPath) + } + } + + // Extract only the file name + var testBundlePath = filepath.Base(data.TestBundlePath) + + // Build the TestConfig object from parsed data + testConfig := TestConfig{ + BundleId: bundleId, + TestRunnerBundleId: data.TestHostBundleIdentifier, + XctestConfigName: testBundlePath, + Args: data.CommandLineArguments, + Env: testEnv, + TestsToRun: testsToRun, + TestsToSkip: testsToSkip, + XcTest: !data.IsUITestBundle, + Device: device, + Listener: listener, + } + + return testConfig, nil +} + +// parseFile reads the .xctestrun file and decodes it into a map +func parseFile(filePath string) ([]testConfiguration, error) { + file, err := os.Open(filePath) + if err != nil { + return []testConfiguration{}, fmt.Errorf("failed to open xctestrun file: %w", err) + } + defer file.Close() + return decode(file) +} + +// decode decodes the binary xctestrun content into the xCTestRunData struct +func decode(r io.Reader) ([]testConfiguration, error) { + // Read the entire content once + xctestrunFileContent, err := io.ReadAll(r) + if err != nil { + return []testConfiguration{}, fmt.Errorf("unable to read xctestrun content: %w", err) + } + + // First, we only parse the version property of the xctestrun file. The rest of the parsing depends on this version. + version, err := getFormatVersion(xctestrunFileContent) + if err != nil { + return []testConfiguration{}, err + } + + switch version { + case 1: + return parseVersion1(xctestrunFileContent) + case 2: + return parseVersion2(xctestrunFileContent) + default: + return []testConfiguration{}, fmt.Errorf("the provided .xctestrun format version %d is not supported", version) + } +} + +// Helper method to get the format version of the xctestrun file +func getFormatVersion(xctestrunFileContent []byte) (int, error) { + + type xCTestRunMetadata struct { + Metadata struct { + Version int `plist:"FormatVersion"` + } `plist:"__xctestrun_metadata__"` + } + + var metadata xCTestRunMetadata + if _, err := plist.Unmarshal(xctestrunFileContent, &metadata); err != nil { + return 0, fmt.Errorf("failed to parse format version: %w", err) + } + + return metadata.Metadata.Version, nil +} + +func parseVersion1(xctestrunFile []byte) ([]testConfiguration, error) { + // xctestrun files in version 1 use a dynamic key for the pListRoot of the TestConfig. As in the 'key' for the TestConfig is the name + // of the app. This forces us to iterate over the root of the plist, instead of using a static struct to decode the xctestrun file. + var pListRoot map[string]interface{} + if _, err := plist.Unmarshal(xctestrunFile, &pListRoot); err != nil { + return []testConfiguration{}, fmt.Errorf("failed to unmarshal plist: %w", err) + } + + for key, value := range pListRoot { + // Skip the metadata object + if key == "__xctestrun_metadata__" { + continue + } + + // Attempt to convert to schemeData + schemeMap, ok := value.(map[string]interface{}) + if !ok { + continue // Skip if not a valid scheme map + } + + // Parse the scheme into schemeData and update the TestConfig + var schemeParsed schemeData + schemeBuf := new(bytes.Buffer) + encoder := plist.NewEncoder(schemeBuf) + if err := encoder.Encode(schemeMap); err != nil { + return []testConfiguration{}, fmt.Errorf("failed to encode scheme %s: %w", key, err) + } + + // Decode the plist buffer into schemeData + decoder := plist.NewDecoder(bytes.NewReader(schemeBuf.Bytes())) + if err := decoder.Decode(&schemeParsed); err != nil { + return []testConfiguration{}, fmt.Errorf("failed to decode scheme %s: %w", key, err) + } + // Convert the return type to table of testConfiguration + return []testConfiguration{{ + Name: "", // No specific name available, leaving it empty + TestTargets: []schemeData{schemeParsed}, + }}, nil + } + return []testConfiguration{}, nil +} + +func parseVersion2(content []byte) ([]testConfiguration, error) { + type xCTestRunVersion2 struct { + ContainerInfo struct { + ContainerName string `plist:"ContainerName"` + } `plist:"ContainerInfo"` + TestConfigurations []testConfiguration `plist:"TestConfigurations"` + } + + var testConfigs xCTestRunVersion2 + if _, err := plist.Unmarshal(content, &testConfigs); err != nil { + return []testConfiguration{}, fmt.Errorf("failed to parse format version: %w", err) + } + + // Check if TestConfigurations is empty + if len(testConfigs.TestConfigurations) == 0 { + return []testConfiguration{}, fmt.Errorf("The .xctestrun file you provided does not contain any test configurations. Please check your test setup and ensure it includes at least one test configuration.") + } + + // Return a table of TestConfigurations + return testConfigs.TestConfigurations, nil +} + +func getBundleId(installedApps []installationproxy.AppInfo, uiTargetAppPath string) string { + var appNameWithSuffix = filepath.Base(uiTargetAppPath) + var uiTargetAppName = strings.TrimSuffix(appNameWithSuffix, ".app") + for _, app := range installedApps { + if app.CFBundleName() == uiTargetAppName { + return app.CFBundleIdentifier() + } + } + return "" +} diff --git a/ios/testmanagerd/xctestrunutils_test.go b/ios/testmanagerd/xctestrunutils_test.go new file mode 100644 index 00000000..ab1f25d1 --- /dev/null +++ b/ios/testmanagerd/xctestrunutils_test.go @@ -0,0 +1,325 @@ +package testmanagerd + +import ( + "testing" + + "github.com/danielpaulus/go-ios/ios" + "github.com/danielpaulus/go-ios/ios/installationproxy" + "github.com/stretchr/testify/assert" +) + +// Helper function to create mock data and parse the .xctestrun file +func setupParsing(t *testing.T, filePath string) []testConfiguration { + // Act: parse version 1 of xctestrun file + xcTestRunData, err := parseFile(filePath) + assert.NoError(t, err, "Failed to parse .xctestrun file") + return xcTestRunData +} + +// Test Parsing an xctestrun file with format version 1 +func TestParsingV1(t *testing.T) { + xcTestRunData := setupParsing(t, "testdata/format_version_1.xctestrun") + var expectedTestConfigurations = []testConfiguration{ + { + Name: "", + TestTargets: []schemeData{ + { + TestHostBundleIdentifier: "com.example.myApp", + TestBundlePath: "__TESTHOST__/PlugIns/RunnerTests.xctest", + EnvironmentVariables: map[string]any{ + "APP_DISTRIBUTOR_ID_OVERRIDE": "com.apple.AppStore", + "OS_ACTIVITY_DT_MODE": "YES", + "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", + "TERM": "dumb", + }, + TestingEnvironmentVariables: map[string]any{ + "DYLD_INSERT_LIBRARIES": "__TESTHOST__/Frameworks/libXCTestBundleInject.dylib", + "XCInjectBundleInto": "unused", + "Test": "xyz", + }, + CommandLineArguments: []string{}, + OnlyTestIdentifiers: []string{ + "TestClass1/testMethod1", + "TestClass2/testMethod1", + }, + SkipTestIdentifiers: []string{ + "TestClass1/testMethod2", + "TestClass2/testMethod2", + }, + IsUITestBundle: true, + }, + }, + }, + } + assert.Equal(t, expectedTestConfigurations, xcTestRunData) +} + +// Test Building test configs from a parsed xctestrun file with format version 1 +func TestBuildTestConfigV1(t *testing.T) { + // Arrange: Create parsed XCTestRunData using the helper function + testConfigurations := setupParsing(t, "testdata/format_version_1.xctestrun") + + // Mock dependencies + mockDevice := ios.DeviceEntry{ + DeviceID: 8110, + } + mockListener := &TestListener{} + + // Act: Convert testConfigSpecification to TestConfig + var testConfigs []TestConfig + for _, testConfigSpecification := range testConfigurations { + for _, r := range testConfigSpecification.TestTargets { + tc, _ := r.buildTestConfig(mockDevice, mockListener, nil) + testConfigs = append(testConfigs, tc) + } + } + + var expected = []TestConfig{ + { + TestRunnerBundleId: "com.example.myApp", + XctestConfigName: "RunnerTests.xctest", + Args: []string{}, + Env: map[string]any{ + "APP_DISTRIBUTOR_ID_OVERRIDE": "com.apple.AppStore", + "OS_ACTIVITY_DT_MODE": "YES", + "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", + "TERM": "dumb", + "DYLD_INSERT_LIBRARIES": "__TESTHOST__/Frameworks/libXCTestBundleInject.dylib", + "XCInjectBundleInto": "unused", + "Test": "xyz", + }, + TestsToRun: []string{ + "TestClass1/testMethod1", + "TestClass2/testMethod1", + }, + TestsToSkip: []string{ + "TestClass1/testMethod2", + "TestClass2/testMethod2", + }, + XcTest: false, + Device: mockDevice, + Listener: mockListener, + }, + } + assert.Equal(t, expected, testConfigs) +} + +// Test Parsing an xctestrun file with format version 2 +func TestParsingV2(t *testing.T) { + testTargets := setupParsing(t, "testdata/format_version_2.xctestrun") + + var expectedTestConfigurations = []testConfiguration{ + { + Name: "Test Scheme Action", + TestTargets: []schemeData{ + { + TestHostBundleIdentifier: "saucelabs.FakeCounterApp", + TestBundlePath: "__TESTHOST__/PlugIns/FakeCounterAppTests.xctest", + EnvironmentVariables: map[string]any{ + "APP_DISTRIBUTOR_ID_OVERRIDE": "com.apple.AppStore", + "OS_ACTIVITY_DT_MODE": "YES", + "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", + "TERM": "dumb", + }, + TestingEnvironmentVariables: map[string]any{ + "DYLD_INSERT_LIBRARIES": "__TESTHOST__/Frameworks/libXCTestBundleInject.dylib", + "XCInjectBundleInto": "unused", + }, + CommandLineArguments: []string{}, + OnlyTestIdentifiers: nil, + SkipTestIdentifiers: []string{ + "SkippedTests", "SkippedTests/testThatAlwaysFailsAndShouldBeSkipped", + }, + IsUITestBundle: false, + UITargetAppEnvironmentVariables: nil, + UITargetAppPath: "", + }, + { + TestHostBundleIdentifier: "saucelabs.FakeCounterAppUITests.xctrunner", + TestBundlePath: "__TESTHOST__/PlugIns/FakeCounterAppUITests.xctest", + EnvironmentVariables: map[string]any{ + "APP_DISTRIBUTOR_ID_OVERRIDE": "com.apple.AppStore", + "OS_ACTIVITY_DT_MODE": "YES", + "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", + "TERM": "dumb", + }, + TestingEnvironmentVariables: map[string]any{}, + CommandLineArguments: []string{}, + OnlyTestIdentifiers: nil, + SkipTestIdentifiers: nil, + IsUITestBundle: true, + UITargetAppEnvironmentVariables: map[string]any{ + "APP_DISTRIBUTOR_ID_OVERRIDE": "com.apple.AppStore", + }, + UITargetAppPath: "__TESTROOT__/Debug-iphoneos/FakeCounterApp.app", + }, + }, + }, + } + + assert.Equal(t, expectedTestConfigurations, testTargets) +} + +// Test Parsing an xctestrun file with format version 2 +func TestParsingV2_Multiple(t *testing.T) { + testTargets := setupParsing(t, "testdata/format_version_2_multiple.xctestrun") + + var expectedTestConfigurations = []testConfiguration{ + { + Name: "TestCounterApp_1", + TestTargets: []schemeData{ + { + TestHostBundleIdentifier: "saucelabs.FakeCounterAppUITests.xctrunner", + TestBundlePath: "__TESTHOST__/PlugIns/FakeCounterAppUITests.xctest", + EnvironmentVariables: map[string]any{ + "APP_DISTRIBUTOR_ID_OVERRIDE": "com.apple.AppStore", + "OS_ACTIVITY_DT_MODE": "YES", + "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", + "TERM": "dumb", + }, + TestingEnvironmentVariables: map[string]any{}, + CommandLineArguments: []string{}, + OnlyTestIdentifiers: nil, + SkipTestIdentifiers: nil, + IsUITestBundle: true, + UITargetAppEnvironmentVariables: map[string]any{ + "APP_DISTRIBUTOR_ID_OVERRIDE": "com.apple.AppStore", + }, + UITargetAppPath: "__TESTROOT__/Debug-iphoneos/FakeCounterApp.app", + }, + { + TestHostBundleIdentifier: "saucelabs.FakeCounterApp", + TestBundlePath: "__TESTHOST__/PlugIns/FakeCounterAppTests.xctest", + EnvironmentVariables: map[string]any{ + "APP_DISTRIBUTOR_ID_OVERRIDE": "com.apple.AppStore", + "OS_ACTIVITY_DT_MODE": "YES", + "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", + "TERM": "dumb", + }, + TestingEnvironmentVariables: map[string]any{ + "DYLD_INSERT_LIBRARIES": "__TESTHOST__/Frameworks/libXCTestBundleInject.dylib", + "XCInjectBundleInto": "unused", + }, + CommandLineArguments: []string{}, + OnlyTestIdentifiers: nil, + SkipTestIdentifiers: []string{ + "SkippedTests", "SkippedTests/testThatAlwaysFailsAndShouldBeSkipped", + }, + IsUITestBundle: false, + UITargetAppEnvironmentVariables: nil, + UITargetAppPath: "", + }, + }, + }, + { + Name: "TestDuplicateApp_2", + TestTargets: []schemeData{ + { + TestHostBundleIdentifier: "saucelabs.FakeCounterDuplicateApp", + TestBundlePath: "__TESTHOST__/PlugIns/FakeCounterDuplicateAppTests.xctest", + SkipTestIdentifiers: nil, + OnlyTestIdentifiers: nil, + IsUITestBundle: false, + CommandLineArguments: []string{}, + EnvironmentVariables: map[string]any{ + "APP_DISTRIBUTOR_ID_OVERRIDE": "com.apple.AppStore", + "OS_ACTIVITY_DT_MODE": "YES", + "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", + "TERM": "dumb", + }, + TestingEnvironmentVariables: map[string]any{ + "DYLD_INSERT_LIBRARIES": "__TESTHOST__/Frameworks/libXCTestBundleInject.dylib", + "XCInjectBundleInto": "unused", + }, + UITargetAppEnvironmentVariables: nil, + UITargetAppPath: "", + }, + { + TestHostBundleIdentifier: "saucelabs.FakeCounterDuplicateAppUITests.xctrunner", + TestBundlePath: "__TESTHOST__/PlugIns/FakeCounterDuplicateAppUITests.xctest", + SkipTestIdentifiers: nil, + OnlyTestIdentifiers: nil, + IsUITestBundle: true, + CommandLineArguments: []string{}, + EnvironmentVariables: map[string]any{ + "APP_DISTRIBUTOR_ID_OVERRIDE": "com.apple.AppStore", + "OS_ACTIVITY_DT_MODE": "YES", + "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", + "TERM": "dumb", + }, + TestingEnvironmentVariables: map[string]any{}, + UITargetAppEnvironmentVariables: map[string]any{ + "APP_DISTRIBUTOR_ID_OVERRIDE": "com.apple.AppStore", + }, + UITargetAppPath: "__TESTROOT__/Debug-iphoneos/FakeCounterDuplicateApp.app", + }, + }, + }, + } + + assert.Equal(t, expectedTestConfigurations, testTargets) +} + +func TestBuildTestConfigV2(t *testing.T) { + testConfigurations := setupParsing(t, "testdata/format_version_2.xctestrun") + + // Mock dependencies + mockDevice := ios.DeviceEntry{ + DeviceID: 8110, + } + mockListener := &TestListener{} + // Build allApps mock data to verify the getBundleID function + allAppsMockData := []installationproxy.AppInfo{ + { + "CFBundleName": "FakeCounterApp", + "CFBundleIdentifier": "saucelabs.FakeCounterApp", + }, + } + var testConfigs []TestConfig + for _, testConfigSpecification := range testConfigurations { + for _, r := range testConfigSpecification.TestTargets { + tc, _ := r.buildTestConfig(mockDevice, mockListener, allAppsMockData) + testConfigs = append(testConfigs, tc) + } + } + + var expected = []TestConfig{ + { + TestRunnerBundleId: "saucelabs.FakeCounterApp", + XctestConfigName: "FakeCounterAppTests.xctest", + Args: []string{}, + Env: map[string]any{}, + TestsToRun: nil, + TestsToSkip: []string{"SkippedTests", "SkippedTests/testThatAlwaysFailsAndShouldBeSkipped"}, + XcTest: true, + Device: mockDevice, + Listener: mockListener, + }, + { + BundleId: "saucelabs.FakeCounterApp", + TestRunnerBundleId: "saucelabs.FakeCounterAppUITests.xctrunner", + XctestConfigName: "FakeCounterAppUITests.xctest", + Args: []string{}, + Env: map[string]any{ + "APP_DISTRIBUTOR_ID_OVERRIDE": "com.apple.AppStore", + "OS_ACTIVITY_DT_MODE": "YES", + "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", + "TERM": "dumb", + }, + TestsToRun: nil, + TestsToSkip: nil, + XcTest: false, + Device: mockDevice, + Listener: mockListener, + }, + } + assert.Equal(t, expected, testConfigs) +} + +// Test When we use an invalid xctestrun file. +func TestParseXCTestRunFormatV2ThrowsErrorEmptyTestConfigurations(t *testing.T) { + // Act: Use the codec to parse the temp file + _, err := parseFile("testdata/contains_invalid_test_configuration.xctestrun") + // Assert the Error Message + assert.Equal(t, "The .xctestrun file you provided does not contain any test configurations. Please check your test setup and ensure it includes at least one test configuration.", err.Error(), "Error Message mismatch") +} diff --git a/ios/testmanagerd/xcuitestrunner.go b/ios/testmanagerd/xcuitestrunner.go index 58923be0..d9e955c7 100644 --- a/ios/testmanagerd/xcuitestrunner.go +++ b/ios/testmanagerd/xcuitestrunner.go @@ -5,9 +5,11 @@ import ( "errors" "fmt" "io" + "maps" "path" "strings" + "github.com/Masterminds/semver" "github.com/danielpaulus/go-ios/ios/appservice" "github.com/danielpaulus/go-ios/ios/house_arrest" @@ -219,94 +221,106 @@ const ( const testBundleSuffix = "UITests.xctrunner" -func RunXCUITest(bundleID string, testRunnerBundleID string, xctestConfigName string, device ios.DeviceEntry, env []string, 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 - } +// 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 +} - apps, err := installationProxy.BrowseUserApps() +func StartXCTestWithConfig(ctx context.Context, xctestrunFilePath string, device ios.DeviceEntry, listener *TestListener) ([]TestSuite, error) { + xctestConfigurations, err := parseFile(xctestrunFilePath) if err != nil { - return make([]TestSuite, 0), fmt.Errorf("RunXCUITest: cannot browse user apps: %w", err) + return nil, fmt.Errorf("error parsing xctestrun file: %w", err) + } + installedApps := getUserInstalledApps(err, device) + var xcTestTargets []TestConfig + for _, xctestSpecification := range xctestConfigurations { + for i, r := range xctestSpecification.TestTargets { + tc, err := r.buildTestConfig(device, listener, installedApps) + if err != nil { + return nil, fmt.Errorf("building test config at index %d: %w", i, err) + } + xcTestTargets = append(xcTestTargets, tc) + } } - if bundleID != "" && xctestConfigName == "" { - info, err := getappInfo(bundleID, apps) + var results []TestSuite + var targetErrors []error + for _, target := range xcTestTargets { + listener.reset() + suites, err := RunTestWithConfig(ctx, target) if err != nil { - return make([]TestSuite, 0), fmt.Errorf("RunXCUITest: cannot get app information: %w", err) + targetErrors = append(targetErrors, err) } - - xctestConfigName = info.bundleName + "UITests.xctest" + results = append(results, suites...) } - return RunXCUIWithBundleIdsCtx(context.TODO(), bundleID, testRunnerBundleID, xctestConfigName, device, nil, env, testsToRun, testsToSkip, testListener, isXCTest) + return results, errors.Join(targetErrors...) } -func RunXCUIWithBundleIdsCtx( - ctx context.Context, - bundleID string, - testRunnerBundleID string, - xctestConfigFileName string, - device ios.DeviceEntry, - args []string, - env []string, - 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) + 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) + 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) + return runXUITestWithBundleIdsXcode15Ctx(ctx, testConfig, version) } func runXUITestWithBundleIdsXcode15Ctx( ctx context.Context, - bundleID string, - testRunnerBundleID string, - xctestConfigFileName string, - device ios.DeviceEntry, - args []string, - env []string, - 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) } @@ -316,7 +330,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) } @@ -325,8 +339,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) } @@ -335,8 +349,8 @@ func runXUITestWithBundleIdsXcode15Ctx( } testSessionID := uuid.New() - testconfig := createTestConfig(info, testSessionID, xctestConfigFileName, testsToRun, testsToSkip, isXCTest) - 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), @@ -356,26 +370,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) @@ -401,16 +415,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 @@ -425,7 +439,7 @@ func runXUITestWithBundleIdsXcode15Ctx( log.Debugf("Done running test") - return testListener.TestSuites, testListener.err + return config.Listener.TestSuites, config.Listener.err } type processKiller interface { @@ -443,7 +457,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 []string, 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) @@ -471,17 +485,21 @@ func startTestRunner17(device ios.DeviceEntry, appserviceConn *appservice.Connec "XCTestSessionIdentifier": strings.ToUpper(sessionIdentifier), } - for _, entrystring := range testEnv { - entry := strings.Split(entrystring, "=") - key := entry[0] - value := entry[1] - env[key] = value - log.Debugf("adding extra env %s=%s", key, value) + if len(testEnv) > 0 { + maps.Copy(env, testEnv) + + for key, value := range testEnv { + log.Debugf("adding extra env %s=%s", key, value) + } } + var opts = map[string]interface{}{} - opts := map[string]interface{}{ - "ActivateSuspended": uint64(1), - "StartSuspendedKey": uint64(0), + if !isXCTest { + opts = map[string]interface{}{ + "ActivateSuspended": uint64(1), + "StartSuspendedKey": uint64(0), + "__ActivateSuspended": uint64(1), + } } appLaunch, err := appserviceConn.LaunchAppWithStdIo( @@ -499,7 +517,7 @@ func startTestRunner17(device ios.DeviceEntry, appserviceConn *appservice.Connec return appLaunch, nil } -func setupXcuiTest(device ios.DeviceEntry, bundleID string, testRunnerBundleID string, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool) (uuid.UUID, string, nskeyedarchiver.XCTestConfiguration, testInfo, error) { +func setupXcuiTest(device ios.DeviceEntry, bundleID string, testRunnerBundleID string, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool, version *semver.Version) (uuid.UUID, string, nskeyedarchiver.XCTestConfiguration, testInfo, error) { testSessionID := uuid.New() installationProxy, err := installationproxy.New(device) if err != nil { @@ -537,7 +555,7 @@ func setupXcuiTest(device ios.DeviceEntry, bundleID string, testRunnerBundleID s return uuid.UUID{}, "", nskeyedarchiver.XCTestConfiguration{}, testInfo{}, err } log.Debugf("creating test config") - testConfigPath, testConfig, err := createTestConfigOnDevice(testSessionID, info, houseArrestService, xctestConfigFileName, testsToRun, testsToSkip, isXCTest) + testConfigPath, testConfig, err := createTestConfigOnDevice(testSessionID, info, houseArrestService, xctestConfigFileName, testsToRun, testsToSkip, isXCTest, version) if err != nil { return uuid.UUID{}, "", nskeyedarchiver.XCTestConfiguration{}, testInfo{}, err } @@ -545,13 +563,14 @@ func setupXcuiTest(device ios.DeviceEntry, bundleID string, testRunnerBundleID s return testSessionID, testConfigPath, testConfig, info, nil } -func createTestConfigOnDevice(testSessionID uuid.UUID, info testInfo, houseArrestService *house_arrest.Connection, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool) (string, nskeyedarchiver.XCTestConfiguration, error) { +func createTestConfigOnDevice(testSessionID uuid.UUID, info testInfo, houseArrestService *house_arrest.Connection, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool, version *semver.Version) (string, nskeyedarchiver.XCTestConfiguration, error) { relativeXcTestConfigPath := path.Join("tmp", testSessionID.String()+".xctestconfiguration") xctestConfigPath := path.Join(info.testApp.homePath, relativeXcTestConfigPath) testBundleURL := path.Join(info.testApp.path, "PlugIns", xctestConfigFileName) - config := nskeyedarchiver.NewXCTestConfiguration(info.targetApp.bundleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, testBundleURL, testsToRun, testsToSkip, isXCTest) + productModuleName := strings.ReplaceAll(xctestConfigFileName, ".xctest", "") + config := nskeyedarchiver.NewXCTestConfiguration(productModuleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, testBundleURL, testsToRun, testsToSkip, isXCTest, version) result, err := nskeyedarchiver.ArchiveXML(config) if err != nil { return "", nskeyedarchiver.XCTestConfiguration{}, err @@ -561,13 +580,13 @@ func createTestConfigOnDevice(testSessionID uuid.UUID, info testInfo, houseArres if err != nil { return "", nskeyedarchiver.XCTestConfiguration{}, err } - return xctestConfigPath, nskeyedarchiver.NewXCTestConfiguration(info.targetApp.bundleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, testBundleURL, testsToRun, testsToSkip, isXCTest), nil + return xctestConfigPath, nskeyedarchiver.NewXCTestConfiguration(productModuleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, testBundleURL, testsToRun, testsToSkip, isXCTest, version), nil } -func createTestConfig(info testInfo, testSessionID uuid.UUID, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool) nskeyedarchiver.XCTestConfiguration { +func createTestConfig(info testInfo, testSessionID uuid.UUID, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool, version *semver.Version) nskeyedarchiver.XCTestConfiguration { // the default value for this generated by Xcode is the target name, and the same name is used for the '.xctest' bundle name per default productModuleName := strings.ReplaceAll(xctestConfigFileName, ".xctest", "") - return nskeyedarchiver.NewXCTestConfiguration(productModuleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, "PlugIns/"+xctestConfigFileName, testsToRun, testsToSkip, isXCTest) + return nskeyedarchiver.NewXCTestConfiguration(productModuleName, testSessionID, info.targetApp.bundleID, info.targetApp.path, "PlugIns/"+xctestConfigFileName, testsToRun, testsToSkip, isXCTest, version) } type testInfo struct { @@ -584,13 +603,13 @@ type appInfo struct { func getappInfo(bundleID string, apps []installationproxy.AppInfo) (appInfo, error) { for _, app := range apps { - if app.CFBundleIdentifier == bundleID { + if app.CFBundleIdentifier() == bundleID { info := appInfo{ - path: app.Path, - bundleName: app.CFBundleName, - bundleID: app.CFBundleIdentifier, + path: app.Path(), + bundleName: app.CFBundleName(), + bundleID: app.CFBundleIdentifier(), } - if home, ok := app.EnvironmentVariables["HOME"].(string); ok { + if home, ok := app.EnvironmentVariables()["HOME"].(string); ok { info.homePath = home } return info, nil @@ -599,3 +618,18 @@ func getappInfo(bundleID string, apps []installationproxy.AppInfo) (appInfo, err return appInfo{}, fmt.Errorf("Did not find test app for '%s' on device. Is it installed?", bundleID) } + +func getUserInstalledApps(err error, device ios.DeviceEntry) []installationproxy.AppInfo { + svc, err := installationproxy.New(device) + if err != nil { + log.WithError(err).Debug("we couldn't create ios device connection") + return nil + } + defer svc.Close() + installedApps, err := svc.BrowseUserApps() + if err != nil { + log.WithError(err).Debug("we couldn't fetch the installed user apps") + return nil + } + return installedApps +} diff --git a/ios/testmanagerd/xcuitestrunner_11.go b/ios/testmanagerd/xcuitestrunner_11.go index 2da86197..20d10054 100644 --- a/ios/testmanagerd/xcuitestrunner_11.go +++ b/ios/testmanagerd/xcuitestrunner_11.go @@ -3,9 +3,9 @@ package testmanagerd import ( "context" "fmt" - "strings" + "maps" - "github.com/danielpaulus/go-ios/ios" + "github.com/Masterminds/semver" dtx "github.com/danielpaulus/go-ios/ios/dtx_codec" "github.com/danielpaulus/go-ios/ios/instruments" log "github.com/sirupsen/logrus" @@ -13,38 +13,30 @@ import ( func runXCUIWithBundleIdsXcode11Ctx( ctx context.Context, - bundleID string, - testRunnerBundleID string, - xctestConfigFileName string, - device ios.DeviceEntry, - args []string, - env []string, - testsToRun []string, - testsToSkip []string, - testListener *TestListener, - isXCTest bool, + config TestConfig, + version *semver.Version, ) ([]TestSuite, error) { log.Debugf("set up xcuitest") - testSessionId, xctestConfigPath, testConfig, testInfo, err := setupXcuiTest(device, bundleID, testRunnerBundleID, xctestConfigFileName, testsToRun, testsToSkip, isXCTest) + testSessionId, xctestConfigPath, testConfig, testInfo, err := setupXcuiTest(config.Device, config.BundleId, config.TestRunnerBundleId, config.XctestConfigName, config.TestsToRun, config.TestsToSkip, config.XcTest, version) if err != nil { return make([]TestSuite, 0), fmt.Errorf("RunXCUIWithBundleIdsXcode11Ctx: cannot create test config: %w", err) } log.Debugf("test session setup ok") - conn, err := dtx.NewUsbmuxdConnection(device, testmanagerd) + conn, err := dtx.NewUsbmuxdConnection(config.Device, testmanagerd) if err != nil { return make([]TestSuite, 0), fmt.Errorf("RunXCUIWithBundleIdsXcode11Ctx: cannot create a usbmuxd connection to testmanagerd: %w", err) } defer conn.Close() - ideDaemonProxy := newDtxProxyWithConfig(conn, testConfig, testListener) + ideDaemonProxy := newDtxProxyWithConfig(conn, testConfig, config.Listener) - conn2, err := dtx.NewUsbmuxdConnection(device, testmanagerd) + conn2, err := dtx.NewUsbmuxdConnection(config.Device, testmanagerd) if err != nil { return make([]TestSuite, 0), fmt.Errorf("RunXCUIWithBundleIdsXcode11Ctx: cannot create a usbmuxd connection to testmanagerd: %w", err) } defer conn2.Close() log.Debug("connections ready") - ideDaemonProxy2 := newDtxProxyWithConfig(conn2, testConfig, testListener) + ideDaemonProxy2 := newDtxProxyWithConfig(conn2, testConfig, config.Listener) ideDaemonProxy2.ideInterface.testConfig = testConfig // TODO: fixme protocolVersion := uint64(25) @@ -53,13 +45,13 @@ func runXCUIWithBundleIdsXcode11Ctx( return make([]TestSuite, 0), fmt.Errorf("RunXCUIWithBundleIdsXcode11Ctx: cannot initiate a test session: %w", err) } - pControl, err := instruments.NewProcessControl(device) + pControl, err := instruments.NewProcessControl(config.Device) if err != nil { return make([]TestSuite, 0), fmt.Errorf("RunXCUIWithBundleIdsXcode11Ctx: cannot connect to process control: %w", err) } defer pControl.Close() - pid, err := startTestRunner11(pControl, xctestConfigPath, testRunnerBundleID, testSessionId.String(), testInfo.testApp.path+"/PlugIns/"+xctestConfigFileName, args, env) + pid, err := startTestRunner11(pControl, xctestConfigPath, config.TestRunnerBundleId, testSessionId.String(), testInfo.testApp.path+"/PlugIns/"+config.XctestConfigName, config.Args, config.Env) if err != nil { return make([]TestSuite, 0), fmt.Errorf("RunXCUIWithBundleIdsXcode11Ctx: cannot start the test runner: %w", err) } @@ -91,7 +83,7 @@ func runXCUIWithBundleIdsXcode11Ctx( log.WithError(conn2.Err()).Error("conn2 closed unexpectedly") } break - case <-testListener.Done(): + case <-config.Listener.Done(): break case <-ctx.Done(): break @@ -106,11 +98,11 @@ func runXCUIWithBundleIdsXcode11Ctx( log.Debugf("Done running test") - return testListener.TestSuites, testListener.err + return config.Listener.TestSuites, config.Listener.err } func startTestRunner11(pControl *instruments.ProcessControl, xctestConfigPath string, bundleID string, - sessionIdentifier string, testBundlePath string, wdaargs []string, wdaenv []string, + sessionIdentifier string, testBundlePath string, wdaargs []string, wdaenv map[string]interface{}, ) (uint64, error) { args := []interface{}{} for _, arg := range wdaargs { @@ -122,12 +114,12 @@ func startTestRunner11(pControl *instruments.ProcessControl, xctestConfigPath st "XCTestSessionIdentifier": sessionIdentifier, } - for _, entrystring := range wdaenv { - entry := strings.Split(entrystring, "=") - key := entry[0] - value := entry[1] - env[key] = value - log.Debugf("adding extra env %s=%s", key, value) + if len(wdaenv) > 0 { + maps.Copy(env, wdaenv) + + for key, value := range wdaenv { + log.Debugf("adding extra env %s=%s", key, value) + } } opts := map[string]interface{}{ diff --git a/ios/testmanagerd/xcuitestrunner_12.go b/ios/testmanagerd/xcuitestrunner_12.go index 930b3978..f4a0ea40 100644 --- a/ios/testmanagerd/xcuitestrunner_12.go +++ b/ios/testmanagerd/xcuitestrunner_12.go @@ -3,39 +3,38 @@ package testmanagerd import ( "context" "fmt" - "strings" + "maps" "time" - "github.com/danielpaulus/go-ios/ios" + "github.com/Masterminds/semver" dtx "github.com/danielpaulus/go-ios/ios/dtx_codec" "github.com/danielpaulus/go-ios/ios/instruments" "github.com/danielpaulus/go-ios/ios/nskeyedarchiver" log "github.com/sirupsen/logrus" ) -func runXUITestWithBundleIdsXcode12Ctx(ctx context.Context, bundleID string, testRunnerBundleID string, xctestConfigFileName string, - device ios.DeviceEntry, args []string, env []string, testsToRun []string, testsToSkip []string, testListener *TestListener, isXCTest bool, +func runXUITestWithBundleIdsXcode12Ctx(ctx context.Context, config TestConfig, version *semver.Version, ) ([]TestSuite, error) { - conn, err := dtx.NewUsbmuxdConnection(device, testmanagerdiOS14) + conn, err := dtx.NewUsbmuxdConnection(config.Device, testmanagerdiOS14) if err != nil { return make([]TestSuite, 0), fmt.Errorf("RunXUITestWithBundleIdsXcode12Ctx: cannot create a usbmuxd connection to testmanagerd: %w", err) } - testSessionId, xctestConfigPath, testConfig, testInfo, err := setupXcuiTest(device, bundleID, testRunnerBundleID, xctestConfigFileName, testsToRun, testsToSkip, isXCTest) + testSessionId, xctestConfigPath, testConfig, testInfo, err := setupXcuiTest(config.Device, config.BundleId, config.TestRunnerBundleId, config.XctestConfigName, config.TestsToRun, config.TestsToSkip, config.XcTest, version) if err != nil { return make([]TestSuite, 0), fmt.Errorf("RunXUITestWithBundleIdsXcode12Ctx: cannot setup test config: %w", err) } defer conn.Close() - ideDaemonProxy := newDtxProxyWithConfig(conn, testConfig, testListener) + ideDaemonProxy := newDtxProxyWithConfig(conn, testConfig, config.Listener) - conn2, err := dtx.NewUsbmuxdConnection(device, testmanagerdiOS14) + conn2, err := dtx.NewUsbmuxdConnection(config.Device, testmanagerdiOS14) if err != nil { return make([]TestSuite, 0), fmt.Errorf("RunXUITestWithBundleIdsXcode12Ctx: cannot create a usbmuxd connection to testmanagerd: %w", err) } defer conn2.Close() log.Debug("connections ready") - ideDaemonProxy2 := newDtxProxyWithConfig(conn2, testConfig, testListener) + ideDaemonProxy2 := newDtxProxyWithConfig(conn2, testConfig, config.Listener) ideDaemonProxy2.ideInterface.testConfig = testConfig caps, err := ideDaemonProxy.daemonConnection.initiateControlSessionWithCapabilities(nskeyedarchiver.XCTCapabilities{}) if err != nil { @@ -53,13 +52,13 @@ func runXUITestWithBundleIdsXcode12Ctx(ctx context.Context, bundleID string, tes return make([]TestSuite, 0), fmt.Errorf("RunXUITestWithBundleIdsXcode12Ctx: cannot initiate a session with identifier and capabilities: %w", err) } log.Debug(caps2) - pControl, err := instruments.NewProcessControl(device) + pControl, err := instruments.NewProcessControl(config.Device) if err != nil { return make([]TestSuite, 0), fmt.Errorf("RunXUITestWithBundleIdsXcode12Ctx: cannot connect to process control: %w", err) } defer pControl.Close() - pid, err := startTestRunner12(pControl, xctestConfigPath, testRunnerBundleID, testSessionId.String(), testInfo.testApp.path+"/PlugIns/"+xctestConfigFileName, args, env) + pid, err := startTestRunner12(pControl, xctestConfigPath, config.TestRunnerBundleId, testSessionId.String(), testInfo.testApp.path+"/PlugIns/"+config.XctestConfigName, config.Args, config.Env) if err != nil { return make([]TestSuite, 0), fmt.Errorf("RunXUITestWithBundleIdsXcode12Ctx: cannot start test runner: %w", err) } @@ -89,7 +88,7 @@ func runXUITestWithBundleIdsXcode12Ctx(ctx context.Context, bundleID string, tes log.WithError(conn2.Err()).Error("conn2 closed unexpectedly") } break - case <-testListener.Done(): + case <-config.Listener.Done(): break case <-ctx.Done(): break @@ -104,11 +103,11 @@ func runXUITestWithBundleIdsXcode12Ctx(ctx context.Context, bundleID string, tes log.Debugf("Done running test") - return testListener.TestSuites, testListener.err + return config.Listener.TestSuites, config.Listener.err } func startTestRunner12(pControl *instruments.ProcessControl, xctestConfigPath string, bundleID string, - sessionIdentifier string, testBundlePath string, wdaargs []string, wdaenv []string, + sessionIdentifier string, testBundlePath string, wdaargs []string, wdaenv map[string]interface{}, ) (uint64, error) { args := []interface{}{ "-NSTreatUnknownArgumentsAsOpen", "NO", "-ApplePersistenceIgnoreState", "YES", @@ -130,12 +129,12 @@ func startTestRunner12(pControl *instruments.ProcessControl, xctestConfigPath st "XCTestSessionIdentifier": sessionIdentifier, } - for _, entrystring := range wdaenv { - entry := strings.Split(entrystring, "=") - key := entry[0] - value := entry[1] - env[key] = value - log.Debugf("adding extra env %s=%s", key, value) + if len(wdaenv) > 0 { + maps.Copy(env, wdaenv) + + for key, value := range wdaenv { + log.Debugf("adding extra env %s=%s", key, value) + } } opts := map[string]interface{}{ diff --git a/ios/testmanagerd/xcuitestrunner_test.go b/ios/testmanagerd/xcuitestrunner_test.go index dcac7ff4..812265b3 100644 --- a/ios/testmanagerd/xcuitestrunner_test.go +++ b/ios/testmanagerd/xcuitestrunner_test.go @@ -67,9 +67,17 @@ func TestXcuiTest(t *testing.T) { ctx, stopWda := context.WithCancel(context.Background()) bundleID, testbundleID, xctestconfig := "com.facebook.WebDriverAgentRunner.xctrunner", "com.facebook.WebDriverAgentRunner.xctrunner", "WebDriverAgentRunner.xctest" var wdaargs []string - var wdaenv []string + var wdaenv map[string]interface{} go func() { - _, err := testmanagerd.RunXCUIWithBundleIdsCtx(ctx, bundleID, testbundleID, xctestconfig, device, wdaargs, wdaenv, nil, nil, testmanagerd.NewTestListener(os.Stdout, os.Stdout, os.TempDir())) + _, err := testmanagerd.RunTestWithConfig(ctx, testmanagerd.TestConfig{ + BundleId: bundleID, + TestRunnerBundleId: testbundleID, + XctestConfigName: xctestconfig, + Env: wdaenv, + Args: wdaargs, + Device: device, + Listener: testmanagerd.NewTestListener(os.Stdout, os.Stdout, os.TempDir()), + }) if err != nil { log.WithFields(log.Fields{"error": err}).Fatal("Failed running WDA") errorChannel <- err @@ -82,6 +90,7 @@ func TestXcuiTest(t *testing.T) { select { case <-time.After(time.Second * 50): t.Error("timeout") + stopWda() return case <-wdaStarted: log.Info("wda started successfully") @@ -115,7 +124,7 @@ func signAndInstall(device ios.DeviceEntry) error { svc, _ := installationproxy.New(device) response, err := svc.BrowseUserApps() for _, info := range response { - if bundleId == info.CFBundleIdentifier { + if bundleId == info.CFBundleIdentifier() { log.Info("wda installed, skipping installation") return nil } diff --git a/ios/tunnel/tun_runagent_nix.go b/ios/tunnel/tun_runagent_nix.go new file mode 100644 index 00000000..81a042c8 --- /dev/null +++ b/ios/tunnel/tun_runagent_nix.go @@ -0,0 +1,14 @@ +//go:build !windows + +package tunnel + +import ( + "syscall" +) + +// For *nix OS, create and return *syscall.SysProcAttr with Setsid set to true for running go-ios agent in a new session for persistency. +func createSysProcAttr() *syscall.SysProcAttr { + return &syscall.SysProcAttr{ + Setsid: true, + } +} diff --git a/ios/tunnel/tun_runagent_windows.go b/ios/tunnel/tun_runagent_windows.go new file mode 100644 index 00000000..80f04590 --- /dev/null +++ b/ios/tunnel/tun_runagent_windows.go @@ -0,0 +1,14 @@ +//go:build windows + +package tunnel + +import ( + "syscall" +) + +// For Windows OS, create and return *syscall.SysProcAttr by adding flag CREATE_NEW_PROCESS_GROUP for persistency +func createSysProcAttr() *syscall.SysProcAttr { + return &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, + } +} diff --git a/ios/tunnel/tunnel_api.go b/ios/tunnel/tunnel_api.go index 14690839..69a3012b 100644 --- a/ios/tunnel/tunnel_api.go +++ b/ios/tunnel/tunnel_api.go @@ -26,7 +26,7 @@ var netClient = &http.Client{ } func CloseAgent() error { - _, err := netClient.Get(fmt.Sprintf("http://%s:%d/shutdown", "127.0.0.1", ios.HttpApiPort())) + _, err := netClient.Get(fmt.Sprintf("http://%s:%d/shutdown", ios.HttpApiHost(), ios.HttpApiPort())) if err != nil { return fmt.Errorf("CloseAgent: failed to send shutdown request: %w", err) } @@ -34,7 +34,7 @@ func CloseAgent() error { } func IsAgentRunning() bool { - resp, err := netClient.Get(fmt.Sprintf("http://%s:%d/health", "127.0.0.1", ios.HttpApiPort())) + resp, err := netClient.Get(fmt.Sprintf("http://%s:%d/health", ios.HttpApiHost(), ios.HttpApiPort())) if err != nil { return false } @@ -43,7 +43,7 @@ func IsAgentRunning() bool { func WaitUntilAgentReady() bool { for { slog.Info("Waiting for go-ios agent to be ready...") - resp, err := netClient.Get(fmt.Sprintf("http://%s:%d/ready", "127.0.0.1", ios.HttpApiPort())) + resp, err := netClient.Get(fmt.Sprintf("http://%s:%d/ready", ios.HttpApiHost(), ios.HttpApiPort())) if err != nil { return false } @@ -54,7 +54,7 @@ func WaitUntilAgentReady() bool { } } -func RunAgent(args ...string) error { +func RunAgent(mode string, args ...string) error { if IsAgentRunning() { return nil } @@ -64,8 +64,21 @@ func RunAgent(args ...string) error { return fmt.Errorf("RunAgent: failed to get executable path: %w", err) } - cmd := exec.Command(ex, append([]string{"tunnel", "start"}, args...)...) + var cmd *exec.Cmd + switch mode { + case "kernel": + cmd = exec.Command(ex, append([]string{"tunnel", "start"}, args...)...) + case "user": + cmd = exec.Command(ex, append([]string{"tunnel", "start", "--userspace"}, args...)...) + default: + return fmt.Errorf("RunAgent: unknown mode: %s. Only 'kernel' and 'user' are supported", mode) + } + + // OS specific SysProcAttr assignment + cmd.SysProcAttr = createSysProcAttr() + err = cmd.Start() + if err != nil { return fmt.Errorf("RunAgent: failed to start agent: %w", err) } @@ -155,11 +168,11 @@ func ServeTunnelInfo(tm *TunnelManager, port int) error { return nil } -func TunnelInfoForDevice(udid string, tunnelInfoPort int) (Tunnel, error) { +func TunnelInfoForDevice(udid string, tunnelInfoHost string, tunnelInfoPort int) (Tunnel, error) { c := http.Client{ Timeout: 5 * time.Second, } - res, err := c.Get(fmt.Sprintf("http://127.0.0.1:%d/tunnel/%s", tunnelInfoPort, udid)) + res, err := c.Get(fmt.Sprintf("http://%s:%d/tunnel/%s", tunnelInfoHost, tunnelInfoPort, udid)) if err != nil { return Tunnel{}, fmt.Errorf("TunnelInfoForDevice: failed to get tunnel info: %w", err) } @@ -177,11 +190,11 @@ func TunnelInfoForDevice(udid string, tunnelInfoPort int) (Tunnel, error) { return info, nil } -func ListRunningTunnels(tunnelInfoPort int) ([]Tunnel, error) { +func ListRunningTunnels(tunnelInfoHost string, tunnelInfoPort int) ([]Tunnel, error) { c := http.Client{ Timeout: 5 * time.Second, } - res, err := c.Get(fmt.Sprintf("http://127.0.0.1:%d/tunnels", tunnelInfoPort)) + res, err := c.Get(fmt.Sprintf("http://%s:%d/tunnels", tunnelInfoHost, tunnelInfoPort)) if err != nil { return nil, fmt.Errorf("TunnelInfoForDevice: failed to get tunnel info: %w", err) } @@ -382,6 +395,9 @@ func (m manualPairingTunnelStart) StartTunnel(ctx context.Context, device ios.De return ConnectTunnelLockdown(device) } if version.Major() >= 17 { + if userspaceTUN { + return Tunnel{}, errors.New("manualPairingTunnelStart: userspaceTUN not supported for iOS >=17 and < 17.4") + } return ManualPairAndConnectToTunnel(ctx, device, p) } return Tunnel{}, fmt.Errorf("manualPairingTunnelStart: unsupported iOS version %s", version.String()) diff --git a/ios/tunnel/userspace_tunnel.go b/ios/tunnel/userspace_tunnel.go index 94daf77b..a46c235b 100644 --- a/ios/tunnel/userspace_tunnel.go +++ b/ios/tunnel/userspace_tunnel.go @@ -8,6 +8,7 @@ import ( "io" "log/slog" "net" + "sync" "time" "github.com/danielpaulus/go-ios/ios" @@ -21,6 +22,22 @@ import ( "gvisor.dev/gvisor/pkg/waiter" ) +// ioResourceCloser is a type for closing function. +type ioResourceCloser func() + +// createIoCloser returns a ioResourceCloser for closing both writer and together +func createIoCloser(rw1, rw2 io.ReadWriteCloser) ioResourceCloser { + + // Using sync.Once is essential to close writer and reader just once + var once sync.Once + return func() { + once.Do(func() { + rw1.Close() + rw2.Close() + }) + } +} + // UserSpaceTUNInterface uses gVisor's netstack to create a userspace virtual network interface. // You can use it to connect local tcp connections to remote adresses on the network. // Set it up with the Init method and provide a io.ReadWriter to a IP/TUN compatible device. @@ -46,9 +63,15 @@ func (iface *UserSpaceTUNInterface) TunnelRWCThroughInterface(localPort uint16, if err != nil { return fmt.Errorf("TunnelRWCThroughInterface: NewEndpoint failed: %+v", err) } + ep.SocketOptions().SetKeepAlive(true) + // Set keep alive idle value more aggresive than the gVisor's 2 hours. NAT and Firewalls can drop the idle connections more aggresive. + p := tcpip.KeepaliveIdleOption(30 * time.Second) + ep.SetSockOpt(&p) + o := tcpip.KeepaliveIntervalOption(1 * time.Second) ep.SetSockOpt(&o) + // Bind if a port is specified. if localPort != 0 { if err := ep.Bind(tcpip.FullAddress{Port: localPort}); err != nil { @@ -78,17 +101,33 @@ func (iface *UserSpaceTUNInterface) TunnelRWCThroughInterface(localPort uint16, return nil } -func proxyConns(rw1 io.ReadWriter, rw2 io.ReadWriter) error { - err1 := make(chan error) - err2 := make(chan error) - go ioCopyWithErr(rw1, rw2, err1) - go ioCopyWithErr(rw2, rw1, err2) - return errors.Join(<-err1, <-err2) +func proxyConns(rw1 io.ReadWriteCloser, rw2 io.ReadWriteCloser) error { + + // Use buffered channel for non-blocking send recieve. We use the same single channel 2 times for 2 ioCopyWithErr. + errCh := make(chan error, 2) + + // Create a IO closing functions to unblock stuck io.Copy() call + ioCloser := createIoCloser(rw1, rw2) + + // Send same error channel and the io close function + go ioCopyWithErr(rw1, rw2, errCh, ioCloser) + go ioCopyWithErr(rw2, rw1, errCh, ioCloser) + + // Read from error channel. As the channel is a FIFO queue first in first out, each <-errCh will read one message and remove it from the channel. + // Order of messages are not important. + err1 := <-errCh + err2 := <-errCh + + return errors.Join(err1, err2) } -func ioCopyWithErr(w io.Writer, r io.Reader, errCh chan error) { +func ioCopyWithErr(w io.Writer, r io.Reader, errCh chan error, ioCloser ioResourceCloser) { _, err := io.Copy(w, r) errCh <- err + + // Close the writer and reader to notify the second io.Copy() if one part of the connection closed. + // This is also necessary to avoid resource leaking. + ioCloser() } // Init initializes the virtual network interface. diff --git a/main.go b/main.go index fdb0955d..ea240dc6 100644 --- a/main.go +++ b/main.go @@ -68,71 +68,76 @@ func Main() { usage := fmt.Sprintf(`go-ios %s Usage: + ios --version | version [options] + ios -h | --help ios activate [options] - ios listen [options] - ios list [options] [--details] - ios info [display | lockdown] [options] - ios image list [options] - ios image mount [--path=] [options] - ios image unmount [options] - ios image auto [--basedir=] [options] - ios syslog [options] - ios screenshot [options] [--output=] [--stream] [--port=] - ios instruments notifications [options] - ios crash ls [] [options] + ios apps [--system] [--all] [--list] [--filesharing] [options] + ios assistivetouch (enable | disable | toggle | get) [--force] [options] + ios ax [--font=] [options] + ios batterycheck [options] + ios batteryregistry [options] ios crash cp [options] + ios crash ls [] [options] ios crash rm [options] - ios devicename [options] ios date [options] - ios timeformat (24h | 12h | toggle | get) [--force] [options] - ios devicestate list [options] + ios debug [options] [--stop-at-entry] + ios devicename [options] ios devicestate enable [options] + ios devicestate list [options] + ios devmode (enable | get) [--enable-post-restart] [options] + ios diagnostics list [options] + ios diskspace [options] + ios dproxy [--binary] [--mode=] [--iface=] [options] ios erase [--force] [options] + ios forward [options] + ios fsync [--app=bundleId] [options] (pull | push) --srcPath= --dstPath= + ios fsync [--app=bundleId] [options] (rm [--r] | tree | mkdir) --path= + ios httpproxy [] [] --p12file= --password= [options] + ios httpproxy remove [options] + ios image auto [--basedir=] [options] + ios image list [options] + ios image mount [--path=] [options] + ios image unmount [options] + ios info [display | lockdown] [options] + ios install --path= [options] + ios instruments notifications [options] + ios ip [options] + ios kill ( | --pid= | --process=) [options] ios lang [--setlocale=] [--setlang=] [options] + ios launch [--wait] [--kill-existing] [--arg=]... [--env=]... [options] + ios list [options] [--details] + ios listen [options] + ios memlimitoff (--process=) [options] ios mobilegestalt ... [--plist] [options] - ios diagnostics list [options] - ios profile list [options] + ios pair [--p12file=] [--password=] [options] + ios pcap [options] [--pid=] [--process=] ios prepare [--skip-all] [--skip=]... [--env=]... [options] - ios ax [--font=] [options] - ios debug [options] [--stop-at-entry] - ios fsync (rm [--r] | tree | mkdir) --path= - ios fsync (pull | push) --srcPath= --dstPath= - ios reboot [options] - ios -h | --help - ios --version | version [options] + ios runxctest [--xctestrun-file-path=] [--log-output=] [options] + ios screenshot [options] [--output=] [--stream] [--port=] ios setlocation [options] [--lat=] [--lon=] ios setlocationgpx [options] [--gpxfilepath=] - ios resetlocation [options] - ios assistivetouch (enable | disable | toggle | get) [--force] [options] - ios voiceover (enable | disable | toggle | get) [--force] [options] - ios zoomtouch (enable | disable | toggle | get) [--force] [options] - ios diskspace [options] - ios batterycheck [options] - ios tunnel start [options] [--pair-record-path=] [--userspace] + ios syslog [--parse] [options] + ios sysmontap [options] + ios timeformat (24h | 12h | toggle | get) [--force] [options] ios tunnel ls [options] - ios tunnel stopagent - ios devmode (enable | get) [--enable-post-restart] [options] - ios rsd ls [options] + ios tunnel start [options] [--pair-record-path=] [--userspace] + ios tunnel stopagent + ios uninstall [options] + ios voiceover (enable | disable | toggle | get) [--force] [options] + ios zoom (enable | disable | toggle | get) [--force] [options] Options: -v --verbose Enable Debug Logging. @@ -156,103 +161,109 @@ The commands work as following: By default, the first device found will be used for a command unless you specify a --udid=some_udid switch. Specify -v for debug logging and -t for dumping every message. + ios --version | version [options] Prints the version + ios -h | --help Prints this screen. ios activate [options] Activate a device - ios listen [options] Keeps a persistent connection open and notifies about newly connected or disconnected devices. - ios list [options] [--details] Prints a list of all connected device's udids. If --details is specified, it includes version, name and model of each device. - ios info [display | lockdown] [options] Prints a dump of device information from the given source. - ios image list [options] List currently mounted developers images' signatures - ios image mount [--path=] [options] Mount a image from - > For iOS 17+ (personalized developer disk images) must point to the "Restore" directory inside the developer disk - ios image unmount [options] Unmount developer disk image - ios image auto [--basedir=] [options] Automatically download correct dev image from the internets and mount it. - > You can specify a dir where images should be cached. - > The default is the current dir. - ios syslog [options] Prints a device's log output - ios screenshot [options] [--output=] [--stream] [--port=] Takes a screenshot and writes it to the current dir or to If --stream is supplied it - > starts an mjpeg server at 0.0.0.0:3333. Use --port to set another port. - ios instruments notifications [options] Listen to application state notifications + 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 assistivetouch (enable | disable | toggle | get) [--force] [options] Enables, disables, toggles, or returns the state of the "AssistiveTouch" software home-screen button. iOS 11+ only (Use --force to try on older versions). + ios ax [--font=] [options] Access accessibility inspector features. + ios batterycheck [options] Prints battery info. + ios batteryregistry [options] Prints battery registry stats like Temperature, Voltage. + ios crash cp [options] copy "file pattern" to the target dir. Ex.: 'ios crash cp "*" "./crashes"' ios crash ls [] [options] run "ios crash ls" to get all crashreports in a list, > or use a pattern like 'ios crash ls "*ips*"' to filter - ios crash cp [options] copy "file pattern" to the target dir. Ex.: 'ios crash cp "*" "./crashes"' ios crash rm [options] remove file pattern from dir. Ex.: 'ios crash rm "." "*"' to delete everything - ios devicename [options] Prints the devicename ios date [options] Prints the device date - ios devicestate list [options] Prints a list of all supported device conditions, like slow network, gpu etc. + ios debug [--stop-at-entry] Start debug with lldb + ios devicename [options] Prints the devicename ios devicestate enable [options] Enables a profile with ids (use the list command to see options). It will only stay active until the process is terminated. > Ex. "ios devicestate enable SlowNetworkCondition SlowNetwork3GGood" + ios devicestate list [options] Prints a list of all supported device conditions, like slow network, gpu etc. + ios devmode (enable | get) [--enable-post-restart] [options] Enable developer mode on the device or check if it is enabled. Can also completely finalize developer mode setup after device is restarted. + ios diagnostics list [options] List diagnostic infos + ios diskspace [options] Prints disk space info. + ios dproxy [--binary] [--mode=] [--iface=] [options] Starts the reverse engineering proxy server. + > It dumps every communication in plain text so it can be implemented easily. + > Use "sudo launchctl unload -w /Library/Apple/System/Library/LaunchDaemons/com.apple.usbmuxd.plist" + > to stop usbmuxd and load to start it again should the proxy mess up things. + > The --binary flag will dump everything in raw binary without any decoding. ios erase [--force] [options] Erase the device. It will prompt you to input y+Enter unless --force is specified. + ios forward [options] Similar to iproxy, forward a TCP connection to the device. + ios fsync [--app=bundleId] [options] (pull | push) --srcPath= --dstPath= Pull or Push file from srcPath to dstPath. + ios fsync [--app=bundleId] [options] (rm [--r] | tree | mkdir) --path= Remove | treeview | mkdir in target path. --r used alongside rm will recursively remove all files and directories from target path. + ios httpproxy [] [] --p12file= [--password=] set global http proxy on supervised device. Use the password argument or set the environment variable 'P12_PASSWORD' + > Specify proxy password either as argument or using the environment var: PROXY_PASSWORD + > Use p12 file and password for silent installation on supervised devices. + ios httpproxy remove [options] Removes the global http proxy config. Only works with http proxies set by go-ios! + ios image auto [--basedir=] [options] Automatically download correct dev image from the internets and mount it. + > You can specify a dir where images should be cached. + > The default is the current dir. + ios image list [options] List currently mounted developers images' signatures + ios image mount [--path=] [options] Mount a image from + > For iOS 17+ (personalized developer disk images) must point to the "Restore" directory inside the developer disk + ios image unmount [options] Unmount developer disk image + ios info [display | lockdown] [options] Prints a dump of device information from the given source. + ios install --path= [options] Specify a .app folder or an installable ipa file that will be installed. + ios instruments notifications [options] Listen to application state notifications + ios ip [options] Uses the live pcap iOS packet capture to wait until it finds one that contains the IP address of the device. + > It relies on the MAC address of the WiFi adapter to know which is the right IP. + > You have to disable the "automatic wifi address"-privacy feature of the device for this to work. + > If you wanna speed it up, open apple maps or similar to force network traffic. + > f.ex. "ios launch com.apple.Maps" + ios kill ( | --pid= | --process=) [options] Kill app with the specified bundleID, process id, or process name on the device. ios lang [--setlocale=] [--setlang=] [options] Sets or gets the Device language. ios lang will print the current language and locale, as well as a list of all supported langs and locales. + 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 list [options] [--details] Prints a list of all connected device's udids. If --details is specified, it includes version, name and model of each device. + ios listen [options] Keeps a persistent connection open and notifies about newly connected or disconnected devices. + ios memlimitoff (--process=) [options] Waives memory limit set by iOS (For instance a Broadcast Extension limit is 50 MB). ios mobilegestalt ... [--plist] [options] Lets you query mobilegestalt keys. Standard output is json but if desired you can get > it in plist format by adding the --plist param. > Ex.: "ios mobilegestalt MainScreenCanvasSizes ArtworkTraits --plist" - ios diagnostics list [options] List diagnostic infos ios pair [--p12file=] [--password=] [options] Pairs the device. If the device is supervised, specify the path to the p12 file > to pair without a trust dialog. Specify the password either with the argument or > by setting the environment variable 'P12_PASSWORD' - ios profile list List the profiles on the device - ios profile remove Remove the profileName from the device - ios profile add [--p12file=] [--password=] Install profile file on the device. If supervised set p12file and password or the environment variable 'P12_PASSWORD' + ios pcap [options] [--pid=] [--process=] Starts a pcap dump of network traffic, use --pid or --process to filter specific processes. ios prepare [--skip-all] [--skip=]... [--env=]...[options] runs WebDriverAgents > specify runtime args and env vars like --env ENV_1=something --env ENV_2=else and --arg ARG1 --arg ARG2 - ios ax [--font=] [options] Access accessibility inspector features. - ios debug [--stop-at-entry] Start debug with lldb - ios fsync (rm [--r] | tree | mkdir) --path= Remove | treeview | mkdir in target path. --r used alongside rm will recursively remove all files and directories from target path. - ios fsync (pull | push) --srcPath= --dstPath= Pull or Push file from srcPath to dstPath. - ios reboot [options] Reboot the given device - ios -h | --help Prints this screen. - ios --version | version [options] Prints the version + ios runxctest [--xctestrun-file-path=] [--log-output=] [options] Run a XCTest. The --xctestrun-file-path specifies the path to the .xctestrun file to configure the test execution. + > If you provide '-' as log output, it prints resuts to stdout. + ios screenshot [options] [--output=] [--stream] [--port=] Takes a screenshot and writes it to the current dir or to If --stream is supplied it + > starts an mjpeg server at 0.0.0.0:3333. Use --port to set another port. ios setlocation [options] [--lat=] [--lon=] Updates the location of the device to the provided by latitude and longitude coordinates. Example: setlocation --lat=40.730610 --lon=-73.935242 ios setlocationgpx [options] [--gpxfilepath=] Updates the location of the device based on the data in a GPX file. Example: setlocationgpx --gpxfilepath=/home/username/location.gpx - ios resetlocation [options] Resets the location of the device to the actual one - ios assistivetouch (enable | disable | toggle | get) [--force] [options] Enables, disables, toggles, or returns the state of the "AssistiveTouch" software home-screen button. iOS 11+ only (Use --force to try on older versions). - ios voiceover (enable | disable | toggle | get) [--force] [options] Enables, disables, toggles, or returns the state of the "VoiceOver" software home-screen button. iOS 11+ only (Use --force to try on older versions). - ios zoom (enable | disable | toggle | get) [--force] [options] Enables, disables, toggles, or returns the state of the "ZoomTouch" software home-screen button. iOS 11+ only (Use --force to try on older versions). + ios syslog [--parse] [options] Prints a device's log output, Use --parse to parse the fields from the log + ios sysmontap Get system stats like MEM, CPU ios timeformat (24h | 12h | toggle | get) [--force] [options] Sets, or returns the state of the "time format". iOS 11+ only (Use --force to try on older versions). - ios diskspace [options] Prints disk space info. - ios batterycheck [options] Prints battery info. + ios tunnel ls List currently started tunnels. Use --enabletun to activate using TUN devices rather than user space network. Requires sudo/admin shells. ios tunnel start [options] [--pair-record-path=] [--enabletun] Creates a tunnel connection to the device. If the device was not paired with the host yet, device pairing will also be executed. > On systems with System Integrity Protection enabled the argument '--pair-record-path=default' can be used to point to /var/db/lockdown/RemotePairing/user_501. > If nothing is specified, the current dir is used for the pair record. > This command needs to be executed with admin privileges. > (On MacOS the process 'remoted' must be paused before starting a tunnel is possible 'sudo pkill -SIGSTOP remoted', and 'sudo pkill -SIGCONT remoted' to resume) - ios tunnel ls List currently started tunnels. Use --enabletun to activate using TUN devices rather than user space network. Requires sudo/admin shells. - ios devmode (enable | get) [--enable-post-restart] [options] Enable developer mode on the device or check if it is enabled. Can also completely finalize developer mode setup after device is restarted. - ios rsd ls [options] List RSD services and their port. + ios voiceover (enable | disable | toggle | get) [--force] [options] Enables, disables, toggles, or returns the state of the "VoiceOver" software home-screen button. iOS 11+ only (Use --force to try on older versions). + ios zoom (enable | disable | toggle | get) [--force] [options] Enables, disables, toggles, or returns the state of the "ZoomTouch" software home-screen button. iOS 11+ only (Use --force to try on older versions). `, version) arguments, err := docopt.ParseDoc(usage) @@ -286,11 +297,12 @@ The commands work as following: log.Debug(arguments) skipAgent, _ := os.LookupEnv("ENABLE_GO_IOS_AGENT") - if skipAgent == "yes" { - tunnel.RunAgent() + if skipAgent == "user" || skipAgent == "kernel" { + tunnel.RunAgent(skipAgent) } + if !tunnel.IsAgentRunning() { - log.Warn("go-ios agent is not running. You might need to start it with 'ios tunnel start' for ios17+. Use ENABLE_GO_IOS_AGENT=yes for experimental daemon mode.") + log.Warn("go-ios agent is not running. You might need to start it with 'ios tunnel start' for ios17+. Use ENABLE_GO_IOS_AGENT=user for userspace tunnel or ENABLE_GO_IOS_AGENT=kernel for kernel tunnel for the experimental daemon mode.") } shouldPrintVersionNoDashes, _ := arguments.Bool("version") shouldPrintVersion, _ := arguments.Bool("--version") @@ -319,6 +331,11 @@ The commands work as following: return } + tunnelInfoHost, err := arguments.String("--tunnel-info-host") + if err != nil { + tunnelInfoHost = ios.HttpApiHost() + } + tunnelInfoPort, err := arguments.Int("--tunnel-info-port") if err != nil { tunnelInfoPort = ios.HttpApiPort() @@ -329,6 +346,11 @@ The commands work as following: udid, _ := arguments.String("--udid") address, addressErr := arguments.String("--address") rsdPort, rsdErr := arguments.Int("--rsd-port") + userspaceTunnelHost, userspaceTunnelHostErr := arguments.String("--userspace-host") + if userspaceTunnelHostErr != nil { + userspaceTunnelHost = ios.HttpApiHost() + } + userspaceTunnelPort, userspaceTunnelErr := arguments.Int("--userspace-port") device, err := ios.GetDevice(udid) @@ -338,13 +360,15 @@ The commands work as following: if addressErr == nil && rsdErr == nil { if userspaceTunnelErr == nil { device.UserspaceTUN = true + device.UserspaceTUNHost = userspaceTunnelHost device.UserspaceTUNPort = userspaceTunnelPort } device = deviceWithRsdProvider(device, udid, address, rsdPort) } else { - info, err := tunnel.TunnelInfoForDevice(device.Properties.SerialNumber, tunnelInfoPort) + info, err := tunnel.TunnelInfoForDevice(device.Properties.SerialNumber, tunnelInfoHost, tunnelInfoPort) if err == nil { device.UserspaceTUNPort = info.UserspaceTUNPort + device.UserspaceTUNHost = userspaceTunnelHost device.UserspaceTUN = info.UserspaceTUN device = deviceWithRsdProvider(device, udid, info.Address, info.RsdPort) } else { @@ -620,7 +644,9 @@ The commands work as following: b, _ = arguments.Bool("syslog") if b { - runSyslog(device) + parse, _ := arguments.Bool("--parse") + + runSyslog(device, parse) return } @@ -817,7 +843,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 { @@ -828,6 +856,33 @@ The commands work as following: } } + b, _ = arguments.Bool("sysmontap") + if b { + printSysmontapStats(device) + } + + b, _ = arguments.Bool("memlimitoff") + if b { + processName, _ := arguments.String("--process") + + pControl, err := instruments.NewProcessControl(device) + exitIfError("processcontrol failed", err) + defer pControl.Close() + + svc, err := instruments.NewDeviceInfoService(device) + exitIfError("failed opening deviceInfoService for getting process list", err) + defer svc.Close() + + processList, _ := svc.ProcessList() + for _, process := range processList { + if process.Pid > 1 && process.Name == processName { + disabled, err := pControl.DisableMemoryLimit(process.Pid) + exitIfError("DisableMemoryLimit failed", err) + log.WithFields(log.Fields{"process": process.Name, "pid": process.Pid}).Info("memory limit is off: ", disabled) + } + } + } + b, _ = arguments.Bool("kill") if b { var response []installationproxy.AppInfo @@ -851,8 +906,8 @@ The commands work as following: exitIfError("browsing apps failed", err) for _, app := range response { - if app.CFBundleIdentifier == bundleID { - processName = app.CFBundleExecutable + if app.CFBundleIdentifier() == bundleID { + processName = app.CFBundleExecutable() break } } @@ -911,10 +966,20 @@ The commands work as following: } rawTestlog, rawTestlogErr := arguments.String("--log-output") - env := arguments["--env"].([]string) - + env := splitKeyValuePairs(arguments["--env"].([]string), "=") isXCTest, _ := arguments.Bool("--xctest") + config := testmanagerd.TestConfig{ + BundleId: bundleID, + TestRunnerBundleId: testRunnerBundleId, + XctestConfigName: xctestConfig, + Env: env, + TestsToRun: testsToRun, + TestsToSkip: testsToSkip, + XcTest: isXCTest, + Device: device, + } + if rawTestlogErr == nil { var writer *os.File = os.Stdout if rawTestlog != "-" { @@ -924,14 +989,17 @@ The commands work as following: } defer writer.Close() - testResults, err := testmanagerd.RunXCUITest(bundleID, testRunnerBundleId, xctestConfig, device, env, testsToRun, testsToSkip, testmanagerd.NewTestListener(writer, writer, os.TempDir()), isXCTest) + config.Listener = testmanagerd.NewTestListener(writer, writer, os.TempDir()) + + testResults, err := testmanagerd.RunTestWithConfig(context.TODO(), config) if err != nil { log.WithFields(log.Fields{"error": err}).Info("Failed running Xcuitest") } log.Info(fmt.Printf("%+v", testResults)) } else { - _, err := testmanagerd.RunXCUITest(bundleID, testRunnerBundleId, xctestConfig, device, env, testsToRun, testsToSkip, testmanagerd.NewTestListener(io.Discard, io.Discard, os.TempDir()), isXCTest) + config.Listener = testmanagerd.NewTestListener(io.Discard, io.Discard, os.TempDir()) + _, err := testmanagerd.RunTestWithConfig(context.TODO(), config) if err != nil { log.WithFields(log.Fields{"error": err}).Info("Failed running Xcuitest") } @@ -939,6 +1007,38 @@ The commands work as following: return } + b, _ = arguments.Bool("runxctest") + if b { + xctestrunFilePath, _ := arguments.String("--xctestrun-file-path") + + rawTestlog, rawTestlogErr := arguments.String("--log-output") + + if rawTestlogErr == nil { + var writer *os.File = os.Stdout + if rawTestlog != "-" { + file, err := os.Create(rawTestlog) + exitIfError("Cannot open file "+rawTestlog, err) + writer = file + } + defer writer.Close() + var listener = testmanagerd.NewTestListener(writer, writer, os.TempDir()) + + testResults, err := testmanagerd.StartXCTestWithConfig(context.TODO(), xctestrunFilePath, device, listener) + if err != nil { + log.WithFields(log.Fields{"error": err}).Info("Failed running Xctest") + } + + log.Info(fmt.Printf("%+v", testResults)) + } else { + var listener = testmanagerd.NewTestListener(io.Discard, io.Discard, os.TempDir()) + _, err := testmanagerd.StartXCTestWithConfig(context.TODO(), xctestrunFilePath, device, listener) + if err != nil { + log.WithFields(log.Fields{"error": err}).Info("Failed running Xctest") + } + } + return + } + if runWdaCommand(device, arguments) { return } @@ -949,6 +1049,12 @@ The commands work as following: return } + b, _ = arguments.Bool("resetax") + if b { + resetAx(device) + return + } + b, _ = arguments.Bool("debug") if b { appPath, _ := arguments.String("") @@ -962,6 +1068,11 @@ The commands work as following: } } + b, _ = arguments.Bool("batteryregistry") + if b { + printBatteryRegistry(device) + } + b, _ = arguments.Bool("reboot") if b { err := diagnostics.Reboot(device) @@ -975,7 +1086,13 @@ The commands work as following: b, _ = arguments.Bool("fsync") if b { - afcService, err := afc.New(device) + containerBundleId, _ := arguments.String("--app") + var afcService *afc.Connection + if containerBundleId == "" { + afcService, err = afc.New(device) + } else { + afcService, err = afc.NewContainer(device, containerBundleId) + } exitIfError("fsync: connect afc service failed", err) b, _ = arguments.Bool("rm") if b { @@ -1075,7 +1192,7 @@ The commands work as following: } startTunnel(context.TODO(), pairRecordsPath, tunnelInfoPort, useUserspaceNetworking) } else if listCommand { - tunnels, err := tunnel.ListRunningTunnels(tunnelInfoPort) + tunnels, err := tunnel.ListRunningTunnels(tunnelInfoHost, tunnelInfoPort) if err != nil { exitIfError("failed to get tunnel infos", err) } @@ -1111,6 +1228,42 @@ The commands work as following: } } +func printSysmontapStats(device ios.DeviceEntry) { + const xcodeDefaultSamplingRate = 10 + sysmon, err := instruments.NewSysmontapService(device, xcodeDefaultSamplingRate) + if err != nil { + exitIfError("systemMonitor creation error", err) + } + defer sysmon.Close() + + cpuUsageChannel := sysmon.ReceiveCPUUsage() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + log.Info("starting to monitor CPU usage... Press CTRL+C to stop.") + + for { + select { + case cpuUsageMsg, ok := <-cpuUsageChannel: + if !ok { + log.Info("CPU usage channel closed.") + return + } + log.WithFields(log.Fields{ + "cpu_count": cpuUsageMsg.CPUCount, + "enabled_cpus": cpuUsageMsg.EnabledCPUs, + "end_time": cpuUsageMsg.EndMachAbsTime, + "cpu_total_load": cpuUsageMsg.SystemCPUUsage.CPU_TotalLoad, + }).Info("received CPU usage data") + + case <-c: + log.Info("shutting down sysmontap") + return + } + } +} + func mobileGestaltCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { b, _ := arguments.Bool("mobilegestalt") if b { @@ -1189,7 +1342,7 @@ func runWdaCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { testbundleID, _ := arguments.String("--testrunnerbundleid") xctestconfig, _ := arguments.String("--xctestconfig") wdaargs := arguments["--arg"].([]string) - wdaenv := arguments["--env"].([]string) + wdaenv := splitKeyValuePairs(arguments["--env"].([]string), "=") if bundleID == "" && testbundleID == "" && xctestconfig == "" { log.Info("no bundle ids specified, falling back to defaults") @@ -1222,7 +1375,15 @@ func runWdaCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { defer close(errorChannel) ctx, stopWda := context.WithCancel(context.Background()) go func() { - _, err := testmanagerd.RunXCUIWithBundleIdsCtx(ctx, bundleID, testbundleID, xctestconfig, device, wdaargs, wdaenv, nil, nil, testmanagerd.NewTestListener(writer, writer, os.TempDir()), false) + _, err := testmanagerd.RunTestWithConfig(ctx, testmanagerd.TestConfig{ + BundleId: bundleID, + TestRunnerBundleId: testbundleID, + XctestConfigName: xctestconfig, + Env: wdaenv, + Args: wdaargs, + Device: device, + Listener: testmanagerd.NewTestListener(writer, writer, os.TempDir()), + }) if err != nil { errorChannel <- err } @@ -1277,6 +1438,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 { @@ -1622,8 +1804,8 @@ func startAx(device ios.DeviceEntry, arguments docopt.Opts) { /* conn.GetElement() time.Sleep(time.Second) conn.TurnOff()*/ - //conn.GetElement() - //conn.GetElement() + // conn.GetElement() + // conn.GetElement() exitIfError("ax failed", err) }() @@ -1632,6 +1814,14 @@ func startAx(device ios.DeviceEntry, arguments docopt.Opts) { <-c } +func resetAx(device ios.DeviceEntry) { + conn, err := accessibility.NewWithoutEventChangeListeners(device) + exitIfError("failed creating ax service", err) + + err = conn.ResetToDefaultAccessibilitySettings() + exitIfError("failed resetting ax", err) +} + func printVersion() { versionMap := map[string]interface{}{ "version": version, @@ -1739,6 +1929,21 @@ func printBatteryDiagnostics(device ios.DeviceEntry) { fmt.Println(convertToJSONString(battery)) } +func printBatteryRegistry(device ios.DeviceEntry) { + conn, err := diagnostics.New(device) + if err != nil { + exitIfError("failed diagnostics service", err) + } + defer conn.Close() + + stats, err := conn.Battery() + if err != nil { + exitIfError("failed to get battery stats", err) + } + + fmt.Println(convertToJSONString(stats)) +} + func printDeviceDate(device ios.DeviceEntry) { allValues, err := ios.GetValues(device) exitIfError("failed getting values", err) @@ -1773,14 +1978,14 @@ func printInstalledApps(device ios.DeviceEntry, system bool, all bool, list bool if list { for _, v := range response { - fmt.Printf("%s %s %s\n", v.CFBundleIdentifier, v.CFBundleName, v.CFBundleShortVersionString) + fmt.Printf("%s %s %s\n", v.CFBundleIdentifier(), v.CFBundleName(), v.CFBundleShortVersionString()) } return } if filesharing { for _, v := range response { - if v.UIFileSharingEnabled { - fmt.Printf("%s %s %s\n", v.CFBundleIdentifier, v.CFBundleName, v.CFBundleShortVersionString) + if v.UIFileSharingEnabled() { + fmt.Printf("%s %s %s\n", v.CFBundleIdentifier(), v.CFBundleName(), v.CFBundleShortVersionString()) } } return @@ -1952,7 +2157,7 @@ func outputProcessListNoJSON(device ios.DeviceEntry, processes []instruments.Pro log.Error("browsing installed apps failed. bundleID will not be included in output") } else { for _, app := range response { - appInfoByExecutableName[app.CFBundleExecutable] = app + appInfoByExecutableName[app.CFBundleExecutable()] = app } } @@ -1974,7 +2179,7 @@ func outputProcessListNoJSON(device ios.DeviceEntry, processes []instruments.Pro bundleID := "" appInfo, exists := appInfoByExecutableName[processInfo.Name] if exists { - bundleID = appInfo.CFBundleIdentifier + bundleID = appInfo.CFBundleIdentifier() } fmt.Printf("%*d %-*s %s %s\n", maxPidLength, processInfo.Pid, maxNameLength, processInfo.Name, processInfo.StartDate.Format("2006-01-02 15:04:05"), bundleID) } @@ -2041,7 +2246,7 @@ func printDeviceInfo(device ios.DeviceEntry) { fmt.Println(convertToJSONString(allValues)) } -func runSyslog(device ios.DeviceEntry) { +func runSyslog(device ios.DeviceEntry, parse bool) { log.Debug("Run Syslog.") syslogConnection, err := syslog.New(device) @@ -2049,8 +2254,16 @@ func runSyslog(device ios.DeviceEntry) { defer syslogConnection.Close() + var logFormatter func(string) string + if JSONdisabled { + logFormatter = rawSyslog + } else if parse { + logFormatter = parsedJsonSyslog() + } else { + logFormatter = legacyJsonSyslog() + } + go func() { - messageContainer := map[string]string{} for { logMessage, err := syslogConnection.ReadLogMessage() if err != nil { @@ -2058,12 +2271,8 @@ func runSyslog(device ios.DeviceEntry) { } logMessage = strings.TrimSuffix(logMessage, "\x00") logMessage = strings.TrimSuffix(logMessage, "\x0A") - if JSONdisabled { - fmt.Println(logMessage) - } else { - messageContainer["msg"] = logMessage - fmt.Println(convertToJSONString(messageContainer)) - } + + fmt.Println(logFormatter(logMessage)) } }() c := make(chan os.Signal, 1) @@ -2071,6 +2280,32 @@ func runSyslog(device ios.DeviceEntry) { <-c } +func rawSyslog(log string) string { + return log +} + +func legacyJsonSyslog() func(log string) string { + messageContainer := map[string]string{} + + return func(log string) string { + messageContainer["msg"] = log + return convertToJSONString(messageContainer) + } +} + +func parsedJsonSyslog() func(log string) string { + parser := syslog.Parser() + + return func(log string) string { + log_entry, err := parser(log) + if err != nil { + return convertToJSONString(map[string]string{"msg": log, "error": err.Error()}) + } + + return convertToJSONString(log_entry) + } +} + func pairDevice(device ios.DeviceEntry, orgIdentityP12File string, p12Password string) { if orgIdentityP12File == "" { err := ios.Pair(device) @@ -2123,6 +2358,7 @@ func deviceWithRsdProvider(device ios.DeviceEntry, udid string, address string, rsdProvider, err := rsdService.Handshake() device1, err := ios.GetDeviceWithAddress(udid, address, rsdProvider) device1.UserspaceTUN = device.UserspaceTUN + device1.UserspaceTUNHost = device.UserspaceTUNHost device1.UserspaceTUNPort = device.UserspaceTUNPort exitIfError("error getting devicelist", err) @@ -2163,3 +2399,14 @@ func exitIfError(msg string, err error) { log.WithFields(log.Fields{"err": err}).Fatalf(msg) } } + +func splitKeyValuePairs(envArgs []string, sep string) map[string]interface{} { + env := make(map[string]interface{}) + for _, entrystring := range envArgs { + entry := strings.Split(entrystring, sep) + key := entry[0] + value := entry[1] + env[key] = value + } + return env +} diff --git a/npm_publish/postinstall.js b/npm_publish/postinstall.js index e193e823..8b5b211b 100644 --- a/npm_publish/postinstall.js +++ b/npm_publish/postinstall.js @@ -12,7 +12,8 @@ var path = require('path'), var ARCH_MAPPING = { "ia32": "386", "x64": "amd64", - "arm": "arm" + "arm": "arm", + "arm64": "arm64" }; // Mapping between Node's `process.platform` to Golang's diff --git a/restapi/api/app_endpoints.go b/restapi/api/app_endpoints.go index 7cfcdd0d..536df08e 100644 --- a/restapi/api/app_endpoints.go +++ b/restapi/api/app_endpoints.go @@ -1,12 +1,17 @@ package api import ( + "log" "net/http" + "os" + "path" "github.com/danielpaulus/go-ios/ios" "github.com/danielpaulus/go-ios/ios/installationproxy" "github.com/danielpaulus/go-ios/ios/instruments" + "github.com/danielpaulus/go-ios/ios/zipconduit" "github.com/gin-gonic/gin" + "github.com/google/uuid" ) // List apps on a device @@ -138,3 +143,95 @@ func KillApp(c *gin.Context) { c.JSON(http.StatusOK, GenericResponse{Message: bundleID + " is not running"}) } + +// Install app on a device +// @Summary Install app on a device +// @Description Install app on a device by uploading an ipa file +// @Tags apps +// @Produce json +// @Param file formData file true "ipa file to install" +// @Success 200 {object} GenericResponse +// @Failure 500 {object} GenericResponse +// @Router /device/{udid}/apps/install [post] +func InstallApp(c *gin.Context) { + device := c.MustGet(IOS_KEY).(ios.DeviceEntry) + file, err := c.FormFile("file") + + log.Printf("Received file: %s", file.Filename) + + if err != nil { + c.JSON(http.StatusUnprocessableEntity, GenericResponse{Error: "file form-data is missing"}) + return + } + + if file.Size == 0 { // 100 MB limit + c.JSON(http.StatusRequestEntityTooLarge, GenericResponse{Error: "uploaded file is empty"}) + return + } + + if file.Size > 200*1024*1024 { // 100 MB limit + c.JSON(http.StatusRequestEntityTooLarge, GenericResponse{Error: "file size exceeds the 200MB limit"}) + return + } + + appDownloadFolder := os.Getenv("APP_DOWNLOAD_FOLDER") + if appDownloadFolder == "" { + appDownloadFolder = os.TempDir() + } + + dst := path.Join(appDownloadFolder, uuid.New().String()+".ipa") + defer func() { + if err := os.Remove(dst); err != nil { + c.JSON(http.StatusInternalServerError, GenericResponse{Error: "failed to delete temporary file"}) + } + }() + + c.SaveUploadedFile(file, dst) + + conn, err := zipconduit.New(device) + if err != nil { + c.JSON(http.StatusInternalServerError, GenericResponse{Error: "Unable to setup ZipConduit connection"}) + return + } + + err = conn.SendFile(dst) + if err != nil { + c.JSON(http.StatusInternalServerError, GenericResponse{Error: "Unable to install uploaded app"}) + return + } + + c.JSON(http.StatusOK, GenericResponse{Message: "App installed successfully"}) +} + +// Uninstall app on a device +// @Summary Uninstall app on a device +// @Description Uninstall app on a device by provided bundleID +// @Tags apps +// @Produce json +// @Param bundleID query string true "bundle identifier of the targeted app" +// @Success 200 {object} GenericResponse +// @Failure 500 {object} GenericResponse +func UninstallApp(c *gin.Context) { + device := c.MustGet(IOS_KEY).(ios.DeviceEntry) + + bundleID := c.Query("bundleID") + if bundleID == "" { + c.JSON(http.StatusUnprocessableEntity, GenericResponse{Error: "bundleID query param is missing"}) + return + } + + svc, err := installationproxy.New(device) + if err != nil { + c.JSON(http.StatusInternalServerError, GenericResponse{Error: err.Error()}) + return + } + defer svc.Close() + + err = svc.Uninstall(bundleID) + if err != nil { + c.JSON(http.StatusInternalServerError, GenericResponse{Error: err.Error()}) + return + } + + c.JSON(http.StatusOK, GenericResponse{Message: bundleID + " uninstalled successfully"}) +} diff --git a/restapi/api/device_endpoints.go b/restapi/api/device_endpoints.go index 2d9a0e3b..8941fa2a 100644 --- a/restapi/api/device_endpoints.go +++ b/restapi/api/device_endpoints.go @@ -3,17 +3,17 @@ package api import ( "bytes" "fmt" - "github.com/danielpaulus/go-ios/ios/imagemounter" - "github.com/danielpaulus/go-ios/ios/mobileactivation" "io" "net/http" "os" "sync" + "github.com/danielpaulus/go-ios/ios/imagemounter" + "github.com/danielpaulus/go-ios/ios/mobileactivation" + "github.com/danielpaulus/go-ios/ios" "github.com/danielpaulus/go-ios/ios/instruments" "github.com/danielpaulus/go-ios/ios/mcinstall" - "github.com/danielpaulus/go-ios/ios/screenshotr" "github.com/danielpaulus/go-ios/ios/simlocation" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" @@ -159,12 +159,22 @@ func Info(c *gin.Context) { // @Router /device/{udid}/screenshot [get] func Screenshot(c *gin.Context) { device := c.MustGet(IOS_KEY).(ios.DeviceEntry) - conn, err := screenshotr.New(device) - log.Error(err) - b, _ := conn.TakeScreenshot() + + screenshotService, err := instruments.NewScreenshotService(device) + if err != nil { + c.JSON(http.StatusInternalServerError, GenericResponse{Error: err.Error()}) + return + } + defer screenshotService.Close() + + imageBytes, err := screenshotService.TakeScreenshot() + if err != nil { + c.JSON(http.StatusInternalServerError, GenericResponse{Error: err.Error()}) + return + } c.Header("Content-Type", "image/png") - c.Data(http.StatusOK, "application/octet-stream", b) + c.Data(http.StatusOK, "application/octet-stream", imageBytes) } // Change the current device location diff --git a/restapi/api/middleware.go b/restapi/api/middleware.go index 12699f9c..b9eadfb8 100644 --- a/restapi/api/middleware.go +++ b/restapi/api/middleware.go @@ -6,7 +6,9 @@ import ( "sync" "github.com/danielpaulus/go-ios/ios" + "github.com/danielpaulus/go-ios/ios/tunnel" "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" ) // DeviceMiddleware makes sure a udid was specified and that a device with that UDID @@ -30,11 +32,52 @@ func DeviceMiddleware() gin.HandlerFunc { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err}) return } + + info, err := tunnel.TunnelInfoForDevice(device.Properties.SerialNumber, ios.HttpApiHost(), ios.HttpApiPort()) + if err == nil { + log.WithField("udid", device.Properties.SerialNumber).Printf("Received tunnel info %v", info) + + device.UserspaceTUNPort = info.UserspaceTUNPort + device.UserspaceTUN = info.UserspaceTUN + + device, err = deviceWithRsdProvider(device, udid, info.Address, info.RsdPort) + if err != nil { + c.Error(err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) // Return an error response + c.Next() + } + } else { + log.WithField("udid", device.Properties.SerialNumber).Warn("failed to get tunnel info") + } + c.Set(IOS_KEY, device) c.Next() } } +func deviceWithRsdProvider(device ios.DeviceEntry, udid string, address string, rsdPort int) (ios.DeviceEntry, error) { + rsdService, err := ios.NewWithAddrPortDevice(address, rsdPort, device) + if err != nil { + return device, err + } + + defer rsdService.Close() + rsdProvider, err := rsdService.Handshake() + if err != nil { + return device, err + } + + device1, err := ios.GetDeviceWithAddress(udid, address, rsdProvider) + if err != nil { + return device, err + } + + device1.UserspaceTUN = device.UserspaceTUN + device1.UserspaceTUNPort = device.UserspaceTUNPort + + return device1, nil +} + const IOS_KEY = "go_ios_device" // LimitNumClientsUDID limits clients to one concurrent connection per device UDID at a time diff --git a/restapi/api/routes.go b/restapi/api/routes.go index 5c6d7a38..b61287c0 100644 --- a/restapi/api/routes.go +++ b/restapi/api/routes.go @@ -38,6 +38,9 @@ func simpleDeviceRoutes(device *gin.RouterGroup) { device.PUT("/setlocation", SetLocation) device.GET("/syslog", streamingMiddleWare, Syslog) + device.POST("/wda/session", CreateWdaSession) + device.GET("/wda/session/:sessionId", ReadWdaSession) + device.DELETE("/wda/session/:sessionId", DeleteWdaSession) } func appRoutes(group *gin.RouterGroup) { @@ -46,4 +49,6 @@ func appRoutes(group *gin.RouterGroup) { router.GET("/", ListApps) router.POST("/launch", LaunchApp) router.POST("/kill", KillApp) + router.POST("/install", InstallApp) + router.POST("/uninstall", UninstallApp) } diff --git a/restapi/api/wda.go b/restapi/api/wda.go new file mode 100644 index 00000000..8af526fa --- /dev/null +++ b/restapi/api/wda.go @@ -0,0 +1,191 @@ +package api + +import ( + "context" + "net/http" + "os" + "sync" + + "github.com/danielpaulus/go-ios/ios" + "github.com/danielpaulus/go-ios/ios/testmanagerd" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +type WdaConfig struct { + BundleID string `json:"bundleId" binding:"required"` + TestbundleID string `json:"testBundleId" binding:"required"` + XCTestConfig string `json:"xcTestConfig" binding:"required"` + Args []string `json:"args"` + Env map[string]interface{} `json:"env"` +} + +type WdaSessionKey struct { + udid string + sessionID string +} + +type WdaSession struct { + Config WdaConfig `json:"config" binding:"required"` + SessionId string `json:"sessionId" binding:"required"` + Udid string `json:"udid" binding:"required"` + stopWda context.CancelFunc +} + +func (session *WdaSession) Write(p []byte) (n int, err error) { + log. + WithField("udid", session.Udid). + WithField("sessionId", session.SessionId). + Debugf("WDA_LOG %s", p) + + return len(p), nil +} + +var globalSessions = sync.Map{} + +// @Summary Create a new WDA session +// @Description Create a new WebDriverAgent session for the specified device +// @Tags WebDriverAgent +// @Accept json +// @Produce json +// @Param config body WdaConfig true "WebDriverAgent Configuration" +// @Success 200 {object} WdaSession +// @Failure 400 {object} GenericResponse +// @Router /wda/session [post] +func CreateWdaSession(c *gin.Context) { + device := c.MustGet(IOS_KEY).(ios.DeviceEntry) + log. + WithField("udid", device.Properties.SerialNumber). + Debugf("Creating WDA session") + + var config WdaConfig + if err := c.ShouldBindJSON(&config); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + sessionKey := WdaSessionKey{ + udid: device.Properties.SerialNumber, + sessionID: uuid.New().String(), + } + + wdaCtx, stopWda := context.WithCancel(context.Background()) + + session := WdaSession{ + Udid: sessionKey.udid, + SessionId: sessionKey.sessionID, + Config: config, + stopWda: stopWda, + } + + go func() { + _, err := testmanagerd.RunTestWithConfig(wdaCtx, testmanagerd.TestConfig{ + BundleId: config.BundleID, + TestRunnerBundleId: config.TestbundleID, + XctestConfigName: config.XCTestConfig, + Env: config.Env, + Args: config.Args, + Device: device, + Listener: testmanagerd.NewTestListener(&session, &session, os.TempDir()), + }) + if err != nil { + log. + WithField("udid", sessionKey.udid). + WithField("sessionId", sessionKey.sessionID). + WithError(err). + Error("Failed running WDA") + } + + stopWda() + globalSessions.Delete(sessionKey) + + log. + WithField("udid", sessionKey.udid). + WithField("sessionId", sessionKey.sessionID). + Debug("Deleted WDA session") + }() + + globalSessions.Store(sessionKey, session) + + log. + WithField("udid", sessionKey.udid). + WithField("sessionId", sessionKey.sessionID). + Debugf("Requested to start WDA session") + + c.JSON(http.StatusOK, session) +} + +// @Summary Get a WebDriverAgent session +// @Description Get a WebDriverAgent session by sessionId +// @Tags WebDriverAgent +// @Produce json +// @Param sessionId path string true "Session ID" +// @Success 200 {object} WdaSession +// @Failure 400 {object} GenericResponse +// @Router /wda/session/{sessionId} [get] +func ReadWdaSession(c *gin.Context) { + device := c.MustGet(IOS_KEY).(ios.DeviceEntry) + + sessionID := c.Param("sessionId") + if sessionID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "sessionId is required"}) + return + } + + sessionKey := WdaSessionKey{ + udid: device.Properties.SerialNumber, + sessionID: sessionID, + } + + session, loaded := globalSessions.Load(sessionKey) + if !loaded { + c.JSON(http.StatusNotFound, gin.H{"error": "session not found"}) + return + } + + c.JSON(http.StatusOK, session) +} + +// @Summary Delete a WebDriverAgent session +// @Description Delete a WebDriverAgent session by sessionId +// @Tags WebDriverAgent +// @Produce json +// @Param sessionId path string true "Session ID" +// @Success 200 {object} WdaSession +// @Failure 400 {object} GenericResponse +// @Router /wda/session/{sessionId} [delete] +func DeleteWdaSession(c *gin.Context) { + device := c.MustGet(IOS_KEY).(ios.DeviceEntry) + + sessionID := c.Param("sessionId") + if sessionID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "sessionId is required"}) + return + } + + sessionKey := WdaSessionKey{ + udid: device.Properties.SerialNumber, + sessionID: sessionID, + } + + session, loaded := globalSessions.Load(sessionKey) + if !loaded { + c.JSON(http.StatusNotFound, gin.H{"error": "session not found"}) + return + } + + wdaSession, ok := session.(WdaSession) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to cast session"}) + return + } + wdaSession.stopWda() + + log. + WithField("udid", sessionKey.udid). + WithField("sessionId", sessionKey.sessionID). + Debug("Requested to stop WDA") + + c.JSON(http.StatusOK, session) +} diff --git a/restapi/go.mod b/restapi/go.mod index e1794ba8..ddc08271 100644 --- a/restapi/go.mod +++ b/restapi/go.mod @@ -1,14 +1,16 @@ module github.com/danielpaulus/go-ios/restapi -go 1.21 +go 1.22.0 + +toolchain go1.22.5 require ( github.com/danielpaulus/go-ios v1.0.91 github.com/gin-gonic/gin v1.8.1 - github.com/sirupsen/logrus v1.8.1 + github.com/sirupsen/logrus v1.9.3 github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a github.com/swaggo/gin-swagger v1.5.2 - github.com/swaggo/swag v1.8.4 + github.com/swaggo/swag v1.16.3 ) require ( @@ -16,7 +18,6 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect - github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect @@ -36,20 +37,26 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/ugorji/go/codec v1.2.7 // indirect - golang.org/x/crypto v0.15.0 // indirect - golang.org/x/net v0.18.0 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.15.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect howett.net/plist v1.0.0 // indirect ) require ( - github.com/frankban/quicktest v1.14.6 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/cenkalti/backoff v2.2.1+incompatible // indirect + github.com/grandcat/zeroconf v1.0.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/miekg/dns v1.1.57 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect + go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect + software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect ) replace github.com/danielpaulus/go-ios => ../ diff --git a/restapi/go.sum b/restapi/go.sum index f5798202..d499bf76 100644 --- a/restapi/go.sum +++ b/restapi/go.sum @@ -8,15 +8,17 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380 h1:1NyRx2f4W4WBRyg0Kys0ZbaNmDDzZ2R/C7DTi+bbsJ0= +github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380/go.mod h1:thX175TtLTzLj3p7N/Q9IiKZ7NF+p72cvL91emV0hzo= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= -github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= @@ -46,12 +48,14 @@ github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= +github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -75,6 +79,9 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= @@ -90,6 +97,8 @@ github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZO github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -98,14 +107,13 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -118,36 +126,44 @@ github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9J github.com/swaggo/gin-swagger v1.5.2 h1:dj2es17EaOHoy0Owu4xn3An1mI8/xjdFyIH6KAbOdYo= github.com/swaggo/gin-swagger v1.5.2/go.mod h1:Cbj/MlHApPOjZdf4joWFXLLgmZVPyh54GPvPPyVjVZM= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= -github.com/swaggo/swag v1.8.4 h1:oGB351qH1JqUqK1tsMYEE5qTBbPk394BhsZxmUfebcI= -github.com/swaggo/swag v1.8.4/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -155,28 +171,28 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= @@ -199,3 +215,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= +software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ=