From 91626bcdd41fa7dd6b804003898a1f411c83cf01 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 23 Oct 2022 22:25:14 +0800 Subject: [PATCH] refactor: move gidevice to hrp pkg --- docs/CHANGELOG.md | 28 +- go.mod | 8 +- go.sum | 3 - hrp/cmd/ios/apps.go | 17 +- hrp/cmd/ios/devices.go | 4 +- hrp/cmd/ios/init.go | 4 +- hrp/pkg/gidevice/.gitignore | 1 + hrp/pkg/gidevice/README.md | 446 +++++++++ hrp/pkg/gidevice/afc.go | 552 ++++++++++ hrp/pkg/gidevice/afc_test.go | 100 ++ hrp/pkg/gidevice/crashreportmover.go | 169 ++++ hrp/pkg/gidevice/crashreportmover_test.go | 41 + hrp/pkg/gidevice/device.go | 940 ++++++++++++++++++ hrp/pkg/gidevice/device_test.go | 174 ++++ hrp/pkg/gidevice/diagnosticsrelay.go | 39 + hrp/pkg/gidevice/housearrest.go | 57 ++ hrp/pkg/gidevice/housearrest_test.go | 58 ++ hrp/pkg/gidevice/idevice.go | 482 +++++++++ hrp/pkg/gidevice/imagemounter.go | 142 +++ hrp/pkg/gidevice/imagemounter_test.go | 49 + hrp/pkg/gidevice/installationproxy.go | 167 ++++ hrp/pkg/gidevice/installationproxy_test.go | 71 ++ hrp/pkg/gidevice/instruments.go | 348 +++++++ hrp/pkg/gidevice/instruments_test.go | 97 ++ hrp/pkg/gidevice/lockdown.go | 684 +++++++++++++ hrp/pkg/gidevice/lockdown_test.go | 106 ++ hrp/pkg/gidevice/pcapd.go | 60 ++ hrp/pkg/gidevice/perfd.go | 889 +++++++++++++++++ hrp/pkg/gidevice/perfd_test.go | 178 ++++ hrp/pkg/gidevice/pkg/ipa/ipa.go | 53 + hrp/pkg/gidevice/pkg/ipa/ipa_test.go | 21 + hrp/pkg/gidevice/pkg/libimobiledevice/afc.go | 105 ++ .../pkg/libimobiledevice/afcmessage.go | 187 ++++ .../pkg/libimobiledevice/auxbuffer.go | 138 +++ .../pkg/libimobiledevice/client_dtxmessage.go | 405 ++++++++ .../libimobiledevice/client_servicepacket.go | 81 ++ .../pkg/libimobiledevice/crashreportmover.go | 20 + .../pkg/libimobiledevice/diagnosticsrelay.go | 39 + .../pkg/libimobiledevice/housearrest.go | 56 ++ .../pkg/libimobiledevice/imagemounter.go | 107 ++ .../pkg/libimobiledevice/installationproxy.go | 119 +++ .../pkg/libimobiledevice/instruments.go | 41 + .../pkg/libimobiledevice/keyedarchiver.go | 260 +++++ hrp/pkg/gidevice/pkg/libimobiledevice/lib.go | 28 + .../gidevice/pkg/libimobiledevice/lockdown.go | 183 ++++ .../pkg/libimobiledevice/packet_afc.go | 86 ++ .../pkg/libimobiledevice/packet_dtxmessage.go | 203 ++++ .../pkg/libimobiledevice/packet_location.go | 53 + .../pkg/libimobiledevice/packet_service.go | 47 + .../pkg/libimobiledevice/packet_usbmux.go | 112 +++ .../gidevice/pkg/libimobiledevice/pcapd.go | 119 +++ .../pkg/libimobiledevice/screenshot.go | 52 + .../pkg/libimobiledevice/simulatelocation.go | 128 +++ .../pkg/libimobiledevice/springboard.go | 53 + .../pkg/libimobiledevice/syslogrelay.go | 21 + .../pkg/libimobiledevice/testmanagerd.go | 45 + .../gidevice/pkg/libimobiledevice/usbmux.go | 414 ++++++++ .../gidevice/pkg/nskeyedarchiver/nsarray.go | 37 + .../pkg/nskeyedarchiver/nsarray_test.go | 18 + .../pkg/nskeyedarchiver/nsdictionary.go | 44 + .../pkg/nskeyedarchiver/nsdictionary_test.go | 18 + .../pkg/nskeyedarchiver/nskeyedarchiver.go | 80 ++ .../nskeyedarchiver/nskeyedarchiver_test.go | 39 + .../gidevice/pkg/nskeyedarchiver/nsnull.go | 27 + hrp/pkg/gidevice/pkg/nskeyedarchiver/nsurl.go | 38 + .../pkg/nskeyedarchiver/nsurl_test.go | 13 + .../gidevice/pkg/nskeyedarchiver/nsuuid.go | 51 + .../pkg/nskeyedarchiver/nsuuid_test.go | 15 + .../pkg/nskeyedarchiver/xctcapabilities.go | 10 + .../nskeyedarchiver/xctestconfiguration.go | 89 ++ .../xctestconfiguration_test.go | 15 + hrp/pkg/gidevice/screenshot.go | 119 +++ hrp/pkg/gidevice/screenshot_test.go | 56 ++ hrp/pkg/gidevice/simulatelocation.go | 27 + hrp/pkg/gidevice/simulatelocation_test.go | 48 + hrp/pkg/gidevice/springboard.go | 68 ++ hrp/pkg/gidevice/springboard_test.go | 53 + hrp/pkg/gidevice/syslogrelay.go | 85 ++ hrp/pkg/gidevice/testmanagerd.go | 50 + hrp/pkg/gidevice/usbmux.go | 153 +++ hrp/pkg/gidevice/usbmux_test.go | 68 ++ hrp/pkg/gidevice/xctestmanagerdaemon.go | 153 +++ hrp/pkg/uixt/ios_device.go | 44 +- hrp/pkg/uixt/ios_driver.go | 6 +- hrp/step.go | 27 +- 85 files changed, 10476 insertions(+), 65 deletions(-) create mode 100644 hrp/pkg/gidevice/.gitignore create mode 100644 hrp/pkg/gidevice/README.md create mode 100644 hrp/pkg/gidevice/afc.go create mode 100644 hrp/pkg/gidevice/afc_test.go create mode 100644 hrp/pkg/gidevice/crashreportmover.go create mode 100644 hrp/pkg/gidevice/crashreportmover_test.go create mode 100644 hrp/pkg/gidevice/device.go create mode 100644 hrp/pkg/gidevice/device_test.go create mode 100644 hrp/pkg/gidevice/diagnosticsrelay.go create mode 100644 hrp/pkg/gidevice/housearrest.go create mode 100644 hrp/pkg/gidevice/housearrest_test.go create mode 100644 hrp/pkg/gidevice/idevice.go create mode 100644 hrp/pkg/gidevice/imagemounter.go create mode 100644 hrp/pkg/gidevice/imagemounter_test.go create mode 100644 hrp/pkg/gidevice/installationproxy.go create mode 100644 hrp/pkg/gidevice/installationproxy_test.go create mode 100644 hrp/pkg/gidevice/instruments.go create mode 100644 hrp/pkg/gidevice/instruments_test.go create mode 100644 hrp/pkg/gidevice/lockdown.go create mode 100644 hrp/pkg/gidevice/lockdown_test.go create mode 100644 hrp/pkg/gidevice/pcapd.go create mode 100644 hrp/pkg/gidevice/perfd.go create mode 100644 hrp/pkg/gidevice/perfd_test.go create mode 100644 hrp/pkg/gidevice/pkg/ipa/ipa.go create mode 100644 hrp/pkg/gidevice/pkg/ipa/ipa_test.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/afc.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/afcmessage.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/auxbuffer.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/client_dtxmessage.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/client_servicepacket.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/crashreportmover.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/diagnosticsrelay.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/housearrest.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/imagemounter.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/installationproxy.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/instruments.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/keyedarchiver.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/lib.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/lockdown.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/packet_afc.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/packet_dtxmessage.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/packet_location.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/packet_service.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/packet_usbmux.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/pcapd.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/screenshot.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/simulatelocation.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/springboard.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/syslogrelay.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/testmanagerd.go create mode 100644 hrp/pkg/gidevice/pkg/libimobiledevice/usbmux.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/nsarray.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/nsarray_test.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/nsdictionary.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/nsdictionary_test.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/nskeyedarchiver.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/nskeyedarchiver_test.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/nsnull.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/nsurl.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/nsurl_test.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/nsuuid.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/nsuuid_test.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/xctcapabilities.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/xctestconfiguration.go create mode 100644 hrp/pkg/gidevice/pkg/nskeyedarchiver/xctestconfiguration_test.go create mode 100644 hrp/pkg/gidevice/screenshot.go create mode 100644 hrp/pkg/gidevice/screenshot_test.go create mode 100644 hrp/pkg/gidevice/simulatelocation.go create mode 100644 hrp/pkg/gidevice/simulatelocation_test.go create mode 100644 hrp/pkg/gidevice/springboard.go create mode 100644 hrp/pkg/gidevice/springboard_test.go create mode 100644 hrp/pkg/gidevice/syslogrelay.go create mode 100644 hrp/pkg/gidevice/testmanagerd.go create mode 100644 hrp/pkg/gidevice/usbmux.go create mode 100644 hrp/pkg/gidevice/usbmux_test.go create mode 100644 hrp/pkg/gidevice/xctestmanagerdaemon.go diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f3aaf35c..94b591a7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,12 +1,23 @@ # Release History -## v4.3.0 (2022-10-21) +## v4.3.0 (2022-10-23) -- feat: support iOS UI automation with [WebDriverAgent] -- feat support Android UI automation with [uiautomator2] -- feat: integrage ios device management with [gidevice] -- feat: add specified exit code for different exceptions -- refactor: make boomer/uixt/httpstat as sub package +Release hrp sub package `uixt` to support iOS/Android UI automation 🎉 + +- feat: support iOS UI automation with [WebDriverAgent] and [gwda] +- feat: support Android UI automation with [uiautomator2] and [guia2] +- feat: support UI recognition with [OCR service] and [gwda-ext-opencv] + +For iOS/Android device management: + +- feat: integrage ios device management with [gidevice], +- feat: integrage android device management with [gadb] +- feat: add simple commands to interact with iOS/Android devices, try `hrp ios` and `hrp adb` + +Other improvements: + +- feat: exit with specified code for different exceptions +- refactor: make uixt/gadb/gidevice/boomer/httpstat as hrp sub package ## v4.2.1 (2022-09-01) @@ -681,3 +692,8 @@ reference: [v2-changelog] [WebDriverAgent]: https://github.com/appium/WebDriverAgent [uiautomator2]: https://github.com/appium/appium-uiautomator2-server [gidevice]: https://github.com/electricbubble/gidevice +[gwda]: https://github.com/electricbubble/gwda +[guia2]: https://github.com/electricbubble/guia2 +[gadb]: https://github.com/electricbubble/gadb +[OCR service]: https://www.volcengine.com/product/text-recognition +[gwda-ext-opencv]: https://github.com/electricbubble/gwda-ext-opencv diff --git a/go.mod b/go.mod index cd7c8c6e..838ee3c0 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.18 require ( github.com/andybalholm/brotli v1.0.4 github.com/denisbrodbeck/machineid v1.0.1 - github.com/electricbubble/gidevice v0.6.2 github.com/electricbubble/opencv-helper v0.0.3 github.com/fatih/color v1.13.0 github.com/getsentry/sentry-go v0.13.0 @@ -18,6 +17,7 @@ require ( github.com/jinzhu/copier v0.3.5 github.com/jmespath/go-jmespath v0.4.0 github.com/json-iterator/go v1.1.12 + github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/maja42/goval v1.2.1 github.com/miekg/dns v1.1.50 github.com/mitchellh/mapstructure v1.5.0 @@ -25,6 +25,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.13.0 github.com/rs/zerolog v1.28.0 + github.com/satori/go.uuid v1.2.0 github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.5.0 github.com/stretchr/testify v1.8.0 @@ -33,6 +34,7 @@ require ( google.golang.org/grpc v1.49.0 google.golang.org/protobuf v1.28.1 gopkg.in/yaml.v3 v3.0.1 + howett.net/plist v1.0.0 ) require ( @@ -52,7 +54,6 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect @@ -68,7 +69,6 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/satori/go.uuid v1.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tklauser/go-sysconf v0.3.10 // indirect github.com/tklauser/numcpus v0.5.0 // indirect @@ -83,8 +83,6 @@ require ( google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - howett.net/plist v1.0.0 // indirect ) // replace github.com/httprunner/funplugin => ../funplugin -replace github.com/electricbubble/gidevice => github.com/debugtalk/gidevice v0.6.3-0.20221012071407-9b59e12ecc77 diff --git a/go.sum b/go.sum index 5f0cae93..c1652ddb 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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/debugtalk/gidevice v0.6.3-0.20221012071407-9b59e12ecc77 h1:wP/2aKW6YV0ityxp0Ecv8JDwA/cy6gayVhA/t+roO+w= -github.com/debugtalk/gidevice v0.6.3-0.20221012071407-9b59e12ecc77/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/electricbubble/opencv-helper v0.0.3 h1:p0sHTUPPPm8GqzVUtYH+wQbJoguzotUXVRAS7Ibk7nI= @@ -918,7 +916,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/hrp/cmd/ios/apps.go b/hrp/cmd/ios/apps.go index d8a62f41..f524c38f 100644 --- a/hrp/cmd/ios/apps.go +++ b/hrp/cmd/ios/apps.go @@ -3,9 +3,10 @@ package ios import ( "fmt" - giDevice "github.com/electricbubble/gidevice" "github.com/mitchellh/mapstructure" "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" ) type Application struct { @@ -24,21 +25,21 @@ var listAppsCmd = &cobra.Command{ return err } - var applicationType giDevice.ApplicationType + var applicationType gidevice.ApplicationType switch appType { case "user": - applicationType = giDevice.ApplicationTypeUser + applicationType = gidevice.ApplicationTypeUser case "system": - applicationType = giDevice.ApplicationTypeSystem + applicationType = gidevice.ApplicationTypeSystem case "internal": - applicationType = giDevice.ApplicationTypeInternal + applicationType = gidevice.ApplicationTypeInternal case "all": - applicationType = giDevice.ApplicationTypeAny + applicationType = gidevice.ApplicationTypeAny } result, errList := device.InstallationProxyBrowse( - giDevice.WithApplicationType(applicationType), - giDevice.WithReturnAttributes("CFBundleVersion", "CFBundleDisplayName", "CFBundleIdentifier")) + gidevice.WithApplicationType(applicationType), + gidevice.WithReturnAttributes("CFBundleVersion", "CFBundleDisplayName", "CFBundleIdentifier")) if errList != nil { return fmt.Errorf("get app list failed") } diff --git a/hrp/cmd/ios/devices.go b/hrp/cmd/ios/devices.go index 0553d82a..bc9724a4 100644 --- a/hrp/cmd/ios/devices.go +++ b/hrp/cmd/ios/devices.go @@ -5,15 +5,15 @@ import ( "fmt" "os" - giDevice "github.com/electricbubble/gidevice" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) type Device struct { - d giDevice.Device + d gidevice.Device UDID string `json:"UDID"` Status string `json:"status"` ConnectionType string `json:"connectionType"` diff --git a/hrp/cmd/ios/init.go b/hrp/cmd/ios/init.go index db1f63a9..209846fb 100644 --- a/hrp/cmd/ios/init.go +++ b/hrp/cmd/ios/init.go @@ -4,9 +4,9 @@ import ( "fmt" "os" - giDevice "github.com/electricbubble/gidevice" "github.com/spf13/cobra" + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) @@ -15,7 +15,7 @@ var iosRootCmd = &cobra.Command{ Short: "simple utils for ios device management", } -func getDevice(udid string) (giDevice.Device, error) { +func getDevice(udid string) (gidevice.Device, error) { devices, err := uixt.IOSDevices(udid) if err != nil { return nil, err diff --git a/hrp/pkg/gidevice/.gitignore b/hrp/pkg/gidevice/.gitignore new file mode 100644 index 00000000..757fee31 --- /dev/null +++ b/hrp/pkg/gidevice/.gitignore @@ -0,0 +1 @@ +/.idea \ No newline at end of file diff --git a/hrp/pkg/gidevice/README.md b/hrp/pkg/gidevice/README.md new file mode 100644 index 00000000..2de44984 --- /dev/null +++ b/hrp/pkg/gidevice/README.md @@ -0,0 +1,446 @@ +# gidevice + +This module is initially forked from [electricbubble/gidevice@v0.6.2]. + +#### Devices + +```go +package main + +import ( + "log" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" +) + +func main() { + usbmux, err := gidevice.NewUsbmux() + if err != nil { + log.Fatalln(err) + } + + devices, err := usbmux.Devices() + if err != nil { + log.Fatal(err) + } + + for _, dev := range devices { + log.Println(dev.Properties().SerialNumber, dev.Properties().ProductID, dev.Properties().DeviceID) + } +} + +``` + +### GetValue + +```go +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" +) + +type DeviceDetail struct { + DeviceName string `json:"DeviceName,omitempty"` + DeviceColor string `json:"DeviceColor,omitempty"` + DeviceClass string `json:"DeviceClass,omitempty"` + ProductVersion string `json:"ProductVersion,omitempty"` + ProductType string `json:"ProductType,omitempty"` + ProductName string `json:"ProductName,omitempty"` + ModelNumber string `json:"ModelNumber,omitempty"` + SerialNumber string `json:"SerialNumber,omitempty"` + SIMStatus string `json:"SIMStatus,omitempty"` + PhoneNumber string `json:"PhoneNumber,omitempty"` + CPUArchitecture string `json:"CPUArchitecture,omitempty"` + ProtocolVersion string `json:"ProtocolVersion,omitempty"` + RegionInfo string `json:"RegionInfo,omitempty"` + TelephonyCapability bool `json:"TelephonyCapability,omitempty"` + TimeZone string `json:"TimeZone,omitempty"` + UniqueDeviceID string `json:"UniqueDeviceID,omitempty"` + WiFiAddress string `json:"WiFiAddress,omitempty"` + WirelessBoardSerialNumber string `json:"WirelessBoardSerialNumber,omitempty"` + BluetoothAddress string `json:"BluetoothAddress,omitempty"` + BuildVersion string `json:"BuildVersion,omitempty"` +} + +func main() { + usbmux, err := gidevice.NewUsbmux() + if err != nil { + log.Fatal(err) + } + + devices, err := usbmux.Devices() + if err != nil { + log.Fatal(err) + } + + if len(devices) == 0 { + log.Fatal("No Device") + } + + d := devices[0] + + detail, err1 := d.GetValue("", "") + if err1 != nil { + fmt.Errorf("get %s device detail fail : %w", d.Properties().SerialNumber, err1) + } + + data, _ := json.Marshal(detail) + d1 := &DeviceDetail{} + json.Unmarshal(data, d1) + fmt.Println(d1) +} +``` + +#### DeveloperDiskImage + +```go +package main + +import ( + "encoding/base64" + "log" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" +) + +func main() { + usbmux, err := gidevice.NewUsbmux() + if err != nil { + log.Fatal(err) + } + + devices, err := usbmux.Devices() + if err != nil { + log.Fatal(err) + } + + if len(devices) == 0 { + log.Fatal("No Device") + } + + d := devices[0] + + imageSignatures, err := d.Images() + if err != nil { + log.Fatalln(err) + } + + for i, imgSign := range imageSignatures { + log.Printf("[%d] %s\n", i+1, base64.StdEncoding.EncodeToString(imgSign)) + } + + dmgPath := "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/14.4/DeveloperDiskImage.dmg" + signaturePath := "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/14.4/DeveloperDiskImage.dmg.signature" + + err = d.MountDeveloperDiskImage(dmgPath, signaturePath) + if err != nil { + log.Fatalln(err) + } +} + +``` + +#### App + +```go +package main + +import ( + "log" + "path/filepath" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" +) + +func main() { + usbmux, err := gidevice.NewUsbmux() + if err != nil { + log.Fatalln(err) + } + + devices, err := usbmux.Devices() + if err != nil { + log.Fatalln(err) + } + + if len(devices) == 0 { + log.Fatalln("No Device") + } + + d := devices[0] + + bundleID := "com.apple.Preferences" + pid, err := d.AppLaunch(bundleID) + if err != nil { + log.Fatalln(err) + } + + err = d.AppKill(pid) + if err != nil { + log.Fatalln(err) + } + + runningProcesses, err := d.AppRunningProcesses() + if err != nil { + log.Fatalln(err) + } + + for _, process := range runningProcesses { + if process.IsApplication { + log.Printf("%4d\t%-24s\t%-36s\t%s\n", process.Pid, process.Name, filepath.Base(process.RealAppName), process.StartDate) + } + } +} + +``` + +#### Screenshot + +```go +package main + +import ( + "image" + "image/jpeg" + "image/png" + "log" + "os" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" +) + +func main() { + usbmux, err := gidevice.NewUsbmux() + if err != nil { + log.Fatalln(err) + } + + devices, err := usbmux.Devices() + if err != nil { + log.Fatalln(err) + } + + if len(devices) == 0 { + log.Fatalln("No Device") + } + + d := devices[0] + + raw, err := d.Screenshot() + if err != nil { + log.Fatalln(err) + } + + img, format, err := image.Decode(raw) + if err != nil { + log.Fatalln(err) + } + userHomeDir, _ := os.UserHomeDir() + file, err := os.Create(userHomeDir + "/Desktop/s1." + format) + if err != nil { + log.Fatalln(err) + } + defer func() { _ = file.Close() }() + switch format { + case "png": + err = png.Encode(file, img) + case "jpeg": + err = jpeg.Encode(file, img, nil) + } + if err != nil { + log.Fatalln(err) + } + log.Println(file.Name()) +} + +``` + +#### SimulateLocation + +```go +package main + +import ( + "log" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" +) + +func main() { + usbmux, err := gidevice.NewUsbmux() + if err != nil { + log.Fatalln(err) + } + + devices, err := usbmux.Devices() + if err != nil { + log.Fatalln(err) + } + + if len(devices) == 0 { + log.Fatalln("No Device") + } + + d := devices[0] + + // https://api.map.baidu.com/lbsapi/getpoint/index.html + if err = d.SimulateLocationUpdate(116.024067, 40.362639, gidevice.CoordinateSystemBD09); err != nil { + log.Fatalln(err) + } + + // https://developer.amap.com/tools/picker + // https://lbs.qq.com/tool/getpoint/index.html + // if err = d.SimulateLocationUpdate(120.116979, 30.252876, gidevice.CoordinateSystemGCJ02); err != nil { + // log.Fatalln(err) + // } + + // if err = d.SimulateLocationUpdate(121.499763, 31.239580,gidevice.CoordinateSystemWGS84); err != nil { + // if err = d.SimulateLocationUpdate(121.499763, 31.239580); err != nil { + // log.Fatalln(err) + // } + + // err = d.SimulateLocationRecover() + // if err != nil { + // log.Fatalln(err) + // } +} + +``` + +#### XCTest + +```go +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" +) + +func main() { + usbmux, err := gidevice.NewUsbmux() + if err != nil { + log.Fatal(err) + } + + devices, err := usbmux.Devices() + if err != nil { + log.Fatal(err) + } + + if len(devices) == 0 { + log.Fatal("No Device") + } + + d := devices[0] + + out, cancel, err := d.XCTest("com.leixipaopao.WebDriverAgentRunner.xctrunner") + if err != nil { + log.Fatal(err) + } + + done := make(chan os.Signal, 1) + signal.Notify(done, os.Interrupt) + + go func() { + for s := range out { + fmt.Print(s) + } + }() + + <-done + cancel() + fmt.Println() + log.Println("DONE") +} + +``` + +#### Connect and Forward + +```go +package main + +import ( + "fmt" + "io" + "log" + "net" + "os" + "os/signal" + "time" + "syscall" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" +) + +func main() { + usbmux, err := gidevice.NewUsbmux() + if err != nil { + log.Fatal(err) + } + + devices, err := usbmux.Devices() + if err != nil { + log.Fatal(err) + } + + if len(devices) == 0 { + log.Fatal("No Device") + } + + d := devices[0] + + localPort, remotePort := 8100, 8100 + + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", localPort)) + + go func(listener net.Listener) { + for { + var accept net.Conn + if accept, err = listener.Accept(); err != nil { + log.Println("accept:", err) + } + + fmt.Println("accept", accept.RemoteAddr()) + + rInnerConn, err := d.NewConnect(remotePort) + if err != nil { + log.Println(err) + os.Exit(0) + } + + rConn := rInnerConn.RawConn() + _ = rConn.SetDeadline(time.Time{}) + + go func(lConn net.Conn) { + go func(lConn, rConn net.Conn) { + if _, err := io.Copy(lConn, rConn); err != nil { + //do sth + } + }(lConn, rConn) + go func(lConn, rConn net.Conn) { + if _, err := io.Copy(rConn, lConn); err != nil { + //do sth + } + }(lConn, rConn) + }(accept) + } + }(listener) + + done := make(chan os.Signal, syscall.SIGTERM) + signal.Notify(done, os.Interrupt, os.Kill) + <-done +} +``` + +[electricbubble/gidevice@v0.6.2]: https://github.com/electricbubble/gidevice/tree/v0.6.2 diff --git a/hrp/pkg/gidevice/afc.go b/hrp/pkg/gidevice/afc.go new file mode 100644 index 00000000..1aa0e323 --- /dev/null +++ b/hrp/pkg/gidevice/afc.go @@ -0,0 +1,552 @@ +package gidevice + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "path" + "strconv" + "time" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +var ErrAfcStatNotExist = errors.New("afc stat: no such file or directory") + +var _ Afc = (*afc)(nil) + +func newAfc(client *libimobiledevice.AfcClient) *afc { + return &afc{client: client} +} + +type afc struct { + client *libimobiledevice.AfcClient +} + +func (c *afc) DiskInfo() (info *AfcDiskInfo, err error) { + if err = c.client.Send(libimobiledevice.AfcOperationGetDeviceInfo, nil, nil); err != nil { + return nil, fmt.Errorf("afc send 'DiskInfo': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return nil, fmt.Errorf("afc receive 'DiskInfo': %w", err) + } + + m := respMsg.Map() + info = &AfcDiskInfo{ + Model: m["Model"], + } + if info.TotalBytes, err = strconv.ParseUint(m["FSTotalBytes"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'DiskInfo': %w", err) + } + if info.FreeBytes, err = strconv.ParseUint(m["FSFreeBytes"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'DiskInfo': %w", err) + } + if info.BlockSize, err = strconv.ParseUint(m["FSBlockSize"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'DiskInfo': %w", err) + } + + return +} + +func (c *afc) ReadDir(dirname string) (names []string, err error) { + if err = c.client.Send(libimobiledevice.AfcOperationReadDir, toCString(dirname), nil); err != nil { + return nil, fmt.Errorf("afc send 'ReadDir': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return nil, fmt.Errorf("afc receive 'ReadDir': %w", err) + } + if err = respMsg.Err(); err != nil { + return nil, fmt.Errorf("afc 'ReadDir': %w", err) + } + + names = respMsg.Strings() + return +} + +func (c *afc) Stat(filename string) (info *AfcFileInfo, err error) { + if err = c.client.Send(libimobiledevice.AfcOperationGetFileInfo, toCString(filename), nil); err != nil { + return nil, fmt.Errorf("afc send 'Stat': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return nil, fmt.Errorf("afc receive 'Stat': %w", err) + } + + m := respMsg.Map() + + if len(m) == 0 { + return nil, ErrAfcStatNotExist + } + + info = &AfcFileInfo{ + source: m, + name: path.Base(filename), + ifmt: m["st_ifmt"], + } + if info.creationTime, err = strconv.ParseUint(m["st_birthtime"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'Stat': %w", err) + } + if info.blocks, err = strconv.ParseUint(m["st_blocks"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'Stat': %w", err) + } + if info.modTime, err = strconv.ParseUint(m["st_mtime"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'Stat': %w", err) + } + if info.nlink, err = strconv.ParseUint(m["st_nlink"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'Stat': %w", err) + } + if info.size, err = strconv.ParseUint(m["st_size"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'Stat': %w", err) + } + + return +} + +func (c *afc) Open(filename string, mode AfcFileMode) (file *AfcFile, err error) { + buf := new(bytes.Buffer) + if err = binary.Write(buf, binary.LittleEndian, uint64(mode)); err != nil { + return nil, fmt.Errorf("afc send 'Open': %w", err) + } + buf.Write(toCString(filename)) + + if err = c.client.Send(libimobiledevice.AfcOperationFileOpen, buf.Bytes(), nil); err != nil { + return nil, fmt.Errorf("afc send 'Open': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return nil, fmt.Errorf("afc receive 'Open': %w", err) + } + if err = respMsg.Err(); err != nil { + return nil, fmt.Errorf("afc 'Open': %w", err) + } + + if respMsg.Operation != libimobiledevice.AfcOperationFileOpenResult { + return nil, fmt.Errorf("afc operation mistake 'Open': '%d'", respMsg.Operation) + } + + file = &AfcFile{ + client: c.client, + fd: respMsg.Uint64(), + } + return +} + +func (c *afc) Remove(filePath string) (err error) { + if err = c.client.Send(libimobiledevice.AfcOperationRemovePath, toCString(filePath), nil); err != nil { + return fmt.Errorf("afc send 'Remove': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'Remove': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'Remove': %w", err) + } + + return +} + +func (c *afc) Rename(oldPath string, newPath string) (err error) { + if err = c.client.Send(libimobiledevice.AfcOperationRenamePath, toCString(oldPath, newPath), nil); err != nil { + return fmt.Errorf("afc send 'Rename': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'Rename': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'Rename': %w", err) + } + + return +} + +func (c *afc) Mkdir(path string) (err error) { + if err = c.client.Send(libimobiledevice.AfcOperationMakeDir, toCString(path), nil); err != nil { + return fmt.Errorf("afc send 'Mkdir': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'Mkdir': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'Mkdir': %w", err) + } + + return +} + +func (c *afc) Link(oldName string, newName string, linkType AfcLinkType) (err error) { + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, uint64(linkType)) + buf.Write(toCString(oldName, newName)) + + if err = c.client.Send(libimobiledevice.AfcOperationMakeLink, buf.Bytes(), nil); err != nil { + return fmt.Errorf("afc send 'Link': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'Link': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'Link': %w", err) + } + + return +} + +func (c *afc) Truncate(filePath string, size int64) (err error) { + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, uint64(size)) + buf.Write(toCString(filePath)) + + if err = c.client.Send(libimobiledevice.AfcOperationTruncateFile, buf.Bytes(), nil); err != nil { + return fmt.Errorf("afc send 'Truncate': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'Truncate': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'Truncate': %w", err) + } + + return +} + +func (c *afc) SetFileModTime(filePath string, modTime time.Time) (err error) { + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, uint64(modTime.Unix())) + buf.Write(toCString(filePath)) + + if err = c.client.Send(libimobiledevice.AfcOperationSetFileModTime, buf.Bytes(), nil); err != nil { + return fmt.Errorf("afc send 'SetFileModTime': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'SetFileModTime': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'SetFileModTime': %w", err) + } + + return +} + +func (c *afc) Hash(filePath string) ([]byte, error) { + var err error + if err = c.client.Send(libimobiledevice.AfcOperationGetFileHash, toCString(filePath), nil); err != nil { + return nil, fmt.Errorf("afc send 'Hash': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return nil, fmt.Errorf("afc receive 'Hash': %w", err) + } + if err = respMsg.Err(); err != nil { + return nil, fmt.Errorf("afc 'Hash': %w", err) + } + + return respMsg.Payload, nil +} + +func (c *afc) HashWithRange(filePath string, start, end uint64) ([]byte, error) { + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, start) + _ = binary.Write(buf, binary.LittleEndian, end) + buf.Write(toCString(filePath)) + + var err error + if err = c.client.Send(libimobiledevice.AfcOperationGetFileHashRange, buf.Bytes(), nil); err != nil { + return nil, fmt.Errorf("afc send 'HashWithRange': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return nil, fmt.Errorf("afc receive 'HashWithRange': %w", err) + } + if err = respMsg.Err(); err != nil { + return nil, fmt.Errorf("afc 'HashWithRange': %w", err) + } + + return respMsg.Payload, nil +} + +// RemoveAll since iOS6+ +func (c *afc) RemoveAll(path string) (err error) { + if err = c.client.Send(libimobiledevice.AfcOperationRemovePathAndContents, toCString(path), nil); err != nil { + return fmt.Errorf("afc send 'RemoveAll': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'RemoveAll': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'RemoveAll': %w", err) + } + + return +} + +func (c *afc) WriteFile(filename string, data []byte, perm AfcFileMode) (err error) { + var file *AfcFile + if file, err = c.Open(filename, perm); err != nil { + return err + } + defer func() { + err = file.Close() + }() + + if _, err = file.Write(data); err != nil { + return err + } + return +} + +func toCString(s ...string) []byte { + buf := new(bytes.Buffer) + for _, v := range s { + buf.WriteString(v) + buf.WriteByte(0) + } + return buf.Bytes() +} + +type AfcDiskInfo struct { + Model string + TotalBytes uint64 + FreeBytes uint64 + BlockSize uint64 +} + +type AfcFileInfo struct { + name string + + creationTime uint64 + blocks uint64 + ifmt string + modTime uint64 + nlink uint64 + size uint64 + + source map[string]string +} + +func (f *AfcFileInfo) Name() string { + return f.name +} + +func (f *AfcFileInfo) Size() int64 { + return int64(f.size) +} + +// func (f *AfcFileInfo) Mode() os.FileMode { +// return os.ModeType +// } + +func (f *AfcFileInfo) ModTime() time.Time { + return time.Unix(0, int64(f.modTime)) +} + +func (f *AfcFileInfo) IsDir() bool { + return f.ifmt == "S_IFDIR" +} + +// func (f *AfcFileInfo) Sys() interface{} { +// return f.source +// } + +func (f *AfcFileInfo) CreationTime() time.Time { + return time.Unix(0, int64(f.creationTime)) +} + +// func (f *AfcFileInfo) Blocks() uint64 { +// return f.blocks +// } + +// func (f *AfcFileInfo) Format() string { +// return f.ifmt +// } + +// func (f *AfcFileInfo) Link() uint64 { +// return f.nlink +// } + +// func (f *AfcFileInfo) PhysicalSize(info *AfcDiskInfo) int64 { +// return int64(f.blocks * (info.BlockSize / 8)) +// } + +type AfcFileMode uint32 + +const ( + AfcFileModeRdOnly AfcFileMode = 0x00000001 + AfcFileModeRw AfcFileMode = 0x00000002 + AfcFileModeWrOnly AfcFileMode = 0x00000003 + AfcFileModeWr AfcFileMode = 0x00000004 + AfcFileModeAppend AfcFileMode = 0x00000005 + AfcFileModeRdAppend AfcFileMode = 0x00000006 +) + +type AfcLockType int + +const ( + AfcLockTypeSharedLock AfcLockType = 1 | 4 + AfcLockTypeExclusiveLock AfcLockType = 2 | 4 + AfcLockTypeUnlock AfcLockType = 8 | 4 +) + +type AfcFile struct { + client *libimobiledevice.AfcClient + fd uint64 + reader *bytes.Reader +} + +func (f *AfcFile) op(o ...uint64) []byte { + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, f.fd) + + if len(o) == 0 { + return buf.Bytes() + } + + for _, v := range o { + _ = binary.Write(buf, binary.LittleEndian, v) + } + + return buf.Bytes() +} + +func (f *AfcFile) Lock(lockType AfcLockType) (err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileRefLock, f.op(uint64(lockType)), nil); err != nil { + return fmt.Errorf("afc file send 'Lock': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = f.client.Receive(); err != nil { + return fmt.Errorf("afc file receive 'Lock': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc file 'Lock': %w", err) + } + return +} + +func (f *AfcFile) Unlock() (err error) { + return f.Lock(AfcLockTypeUnlock) +} + +func (f *AfcFile) Read(b []byte) (n int, err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileRead, f.op(uint64(len(b))), nil); err != nil { + return -1, fmt.Errorf("afc file send 'Read': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = f.client.Receive(); err != nil { + return -1, fmt.Errorf("afc file receive 'Read': %w", err) + } + if err = respMsg.Err(); err != nil { + return -1, fmt.Errorf("afc file 'Read': %w", err) + } + + if respMsg.Payload == nil { + return 0, io.EOF + } + + if f.reader == nil { + f.reader = bytes.NewReader(respMsg.Payload) + } else { + f.reader.Reset(respMsg.Payload) + } + + return f.reader.Read(b) +} + +func (f *AfcFile) Write(b []byte) (n int, err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileWrite, f.op(), b); err != nil { + return -1, fmt.Errorf("afc file send 'Write': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = f.client.Receive(); err != nil { + return -1, fmt.Errorf("afc file receive 'Write': %w", err) + } + if err = respMsg.Err(); err != nil { + return -1, fmt.Errorf("afc file 'Write': %w", err) + } + + n = len(b) + return +} + +func (f *AfcFile) Tell() (n uint64, err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileTell, f.op(), nil); err != nil { + return 0, fmt.Errorf("afc file 'Tell': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = f.client.Receive(); err != nil { + return 0, fmt.Errorf("afc file receive 'Tell': %w", err) + } + if err = respMsg.Err(); err != nil { + return 0, fmt.Errorf("afc file 'Tell': %w", err) + } + + if respMsg.Operation != libimobiledevice.AfcOperationFileTellResult { + return 0, fmt.Errorf("afc operation mistake 'Tell': '%d'", respMsg.Operation) + } + + n = respMsg.Uint64() + return +} + +func (f *AfcFile) Seek(offset int64, whence int) (ret int64, err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileSeek, f.op(uint64(whence), uint64(offset)), nil); err != nil { + return -1, fmt.Errorf("afc file 'Seek': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = f.client.Receive(); err != nil { + return -1, fmt.Errorf("afc file receive 'Seek': %w", err) + } + if err = respMsg.Err(); err != nil { + return -1, fmt.Errorf("afc file 'Seek': %w", err) + } + + var tell uint64 + if tell, err = f.Tell(); err != nil { + return -1, err + } + + ret = int64(tell) + return +} + +func (f *AfcFile) Truncate(size int64) (err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileSetSize, f.op(uint64(size)), nil); err != nil { + return fmt.Errorf("afc file 'Truncate': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = f.client.Receive(); err != nil { + return fmt.Errorf("afc file receive 'Truncate': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc file 'Truncate': %w", err) + } + + return +} + +func (f *AfcFile) Close() (err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileClose, f.op(), nil); err != nil { + return fmt.Errorf("afc file 'Close': %w", err) + } + if _, err = f.client.Receive(); err != nil { + return fmt.Errorf("afc file receive 'Close': %w", err) + } + + return +} + +type AfcLinkType int + +const ( + AfcLinkTypeHardLink AfcLinkType = 1 + AfcLinkTypeSymLink AfcLinkType = 2 +) diff --git a/hrp/pkg/gidevice/afc_test.go b/hrp/pkg/gidevice/afc_test.go new file mode 100644 index 00000000..7a31950a --- /dev/null +++ b/hrp/pkg/gidevice/afc_test.go @@ -0,0 +1,100 @@ +package gidevice + +import ( + "fmt" + "io" + "log" + "os" + "testing" +) + +var afcSrv Afc + +func setupAfcSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if afcSrv, err = lockdownSrv.AfcService(); err != nil { + t.Fatal(err) + } +} + +func Test_afc_DiskInfo(t *testing.T) { + setupAfcSrv(t) + + info, err := afcSrv.DiskInfo() + if err != nil { + t.Fatal(err) + } + log.Printf("%10s: %s\n", "Model", info.Model) + log.Printf("%10s: %d\n", "BlockSize", info.BlockSize/8) + log.Printf("%10s: %s\n", "FreeSpace", byteCountDecimal(int64(info.FreeBytes))) + log.Printf("%10s: %s\n", "UsedSpace", byteCountDecimal(int64(info.TotalBytes-info.FreeBytes))) + log.Printf("%10s: %s\n", "TotalSpace", byteCountDecimal(int64(info.TotalBytes))) +} + +func byteCountDecimal(b int64) string { + const unit = 1000 + if b < unit { + return fmt.Sprintf("%dB", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp]) +} + +func Test_afc_ReadDir(t *testing.T) { + setupAfcSrv(t) + + names, err := afcSrv.ReadDir("Downloads") + if err != nil { + t.Fatal(err) + } + for _, name := range names { + t.Log(name) + } +} + +func Test_afc_Stat(t *testing.T) { + setupAfcSrv(t) + + fileInfo, err := afcSrv.Stat("Downloads/downloads.28.sqlitedb") + if err != nil { + t.Fatal(err) + } + t.Log(fileInfo.Name()) + t.Log(fileInfo.IsDir()) + t.Log(fileInfo.CreationTime()) + t.Log(fileInfo.ModTime()) + t.Log(fileInfo.Size()) + t.Log(byteCountDecimal(fileInfo.Size())) +} + +func Test_afc_Open(t *testing.T) { + setupAfcSrv(t) + + afcFile, err := afcSrv.Open("DCIM/105APPLE/IMG_5977.JPEG", AfcFileModeRdOnly) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = afcFile.Close() + }() + + userHomeDir, _ := os.UserHomeDir() + file, err := os.Create(userHomeDir + "/Desktop/tmp.jpeg") + if err != nil { + t.Fatal(err) + } + + if _, err = io.Copy(file, afcFile); err != nil { + t.Fatal(err) + } +} diff --git a/hrp/pkg/gidevice/crashreportmover.go b/hrp/pkg/gidevice/crashreportmover.go new file mode 100644 index 00000000..71890c98 --- /dev/null +++ b/hrp/pkg/gidevice/crashreportmover.go @@ -0,0 +1,169 @@ +package gidevice + +import ( + "fmt" + "io" + "os" + "path" + "path/filepath" + "strings" + + "howett.net/plist" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +var _ CrashReportMover = (*crashReportMover)(nil) + +func newCrashReportMover(client *libimobiledevice.CrashReportMoverClient) *crashReportMover { + return &crashReportMover{ + client: client, + } +} + +type crashReportMover struct { + client *libimobiledevice.CrashReportMoverClient + afc Afc +} + +func (c *crashReportMover) readPing() (err error) { + var data []byte + if data, err = c.client.InnerConn().Read(4); err != nil { + return err + } + if string(data) != "ping" { + return fmt.Errorf("crashReportMover ping: %v", data) + } + + return +} + +func (c *crashReportMover) Move(hostDir string, opts ...CrashReportMoverOption) (err error) { + opt := defaultCrashReportMoverOption() + for _, fn := range opts { + fn(opt) + } + + toExtract := make([]string, 0, 64) + + fn := func(cwd string, info *AfcFileInfo) { + if info.IsDir() { + return + } + if cwd == "." { + cwd = "" + } + + devFilename := path.Join(cwd, info.Name()) + hostElem := strings.Split(devFilename, "/") + hostFilename := filepath.Join(hostDir, filepath.Join(hostElem...)) + hostFilename = strings.TrimSuffix(hostFilename, ".synced") + + if opt.extract && strings.HasSuffix(hostFilename, ".plist") { + toExtract = append(toExtract, hostFilename) + } + + var afcFile *AfcFile + if afcFile, err = c.afc.Open(devFilename, AfcFileModeRdOnly); err != nil { + debugLog(fmt.Sprintf("crashReportMover open %s: %s", devFilename, err)) + return + } + defer func() { + if err = afcFile.Close(); err != nil { + debugLog(fmt.Sprintf("crashReportMover device file close: %s", err)) + } + }() + + if err = os.MkdirAll(filepath.Dir(hostFilename), 0o755); err != nil { + debugLog(fmt.Sprintf("crashReportMover mkdir %s: %s", filepath.Dir(hostFilename), err)) + return + } + var hostFile *os.File + if hostFile, err = os.Create(hostFilename); err != nil { + debugLog(fmt.Sprintf("crashReportMover create %s: %s", hostFilename, err)) + return + } + defer func() { + if err = hostFile.Close(); err != nil { + debugLog(fmt.Sprintf("crashReportMover host file close: %s", err)) + } + }() + + if _, err = io.Copy(hostFile, afcFile); err != nil { + debugLog(fmt.Sprintf("crashReportMover copy %s", err)) + return + } + + opt.whenDone(devFilename) + + if opt.keep { + return + } + + if err = c.afc.Remove(devFilename); err != nil { + debugLog(fmt.Sprintf("crashReportMover remove %s: %s", devFilename, err)) + return + } + } + if err = c.walkDir(".", fn); err != nil { + return err + } + + if !opt.extract { + return nil + } + + for _, name := range toExtract { + data, err := os.ReadFile(name) + if err != nil { + debugLog(fmt.Sprintf("crashReportMover extract read %s: %s", name, err)) + continue + } + m := make(map[string]interface{}) + if _, err = plist.Unmarshal(data, &m); err != nil { + debugLog(fmt.Sprintf("crashReportMover extract plist %s: %s", name, err)) + continue + } + + desc, ok := m["description"] + if !ok { + continue + } + hostExtCrash := strings.TrimSuffix(name, ".plist") + ".crash" + if err = os.WriteFile(hostExtCrash, []byte(fmt.Sprintf("%v", desc)), 0o755); err != nil { + debugLog(fmt.Sprintf("crashReportMover extract save %s: %s", name, err)) + continue + } + } + + return +} + +func (c *crashReportMover) walkDir(dirname string, fn func(path string, info *AfcFileInfo)) (err error) { + var names []string + if names, err = c.afc.ReadDir(dirname); err != nil { + return err + } + + cwd := dirname + + for _, n := range names { + if n == "." || n == ".." { + continue + } + + var info *AfcFileInfo + if info, err = c.afc.Stat(path.Join(cwd, n)); err != nil { + return err + } + if info.IsDir() { + if err = c.walkDir(path.Join(cwd, info.name), fn); err != nil { + return err + } + } + + fn(cwd, info) + } + + return +} diff --git a/hrp/pkg/gidevice/crashreportmover_test.go b/hrp/pkg/gidevice/crashreportmover_test.go new file mode 100644 index 00000000..dda597ea --- /dev/null +++ b/hrp/pkg/gidevice/crashreportmover_test.go @@ -0,0 +1,41 @@ +package gidevice + +import ( + "fmt" + "os" + "testing" +) + +var crashReportMoverSrv CrashReportMover + +func setupCrashReportMoverSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if crashReportMoverSrv, err = lockdownSrv.CrashReportMoverService(); err != nil { + t.Fatal(err) + } +} + +func Test_crashReportMover_Move(t *testing.T) { + setupCrashReportMoverSrv(t) + + SetDebug(true) + userHomeDir, _ := os.UserHomeDir() + // err := crashReportMoverSrv.Move(userHomeDir + "/Documents/temp/2021-04/out_gidevice") + // err := crashReportMoverSrv.Move(userHomeDir+"/Documents/temp/2021-04/out_gidevice", + err := crashReportMoverSrv.Move(userHomeDir+"/Documents/temp/2021-04/out_gidevice_extract", + WithKeepCrashReport(true), + WithExtractRawCrashReport(true), + WithWhenMoveIsDone(func(filename string) { + fmt.Println("Copy:", filename) + }), + ) + if err != nil { + t.Fatal(err) + } +} diff --git a/hrp/pkg/gidevice/device.go b/hrp/pkg/gidevice/device.go new file mode 100644 index 00000000..00f79783 --- /dev/null +++ b/hrp/pkg/gidevice/device.go @@ -0,0 +1,940 @@ +package gidevice + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "path" + "strings" + "time" + + uuid "github.com/satori/go.uuid" + "howett.net/plist" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/ipa" + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/nskeyedarchiver" +) + +const LockdownPort = 62078 + +var _ Device = (*device)(nil) + +func newDevice(client *libimobiledevice.UsbmuxClient, properties DeviceProperties) *device { + return &device{ + umClient: client, + properties: &properties, + } +} + +type device struct { + umClient *libimobiledevice.UsbmuxClient + lockdownClient *libimobiledevice.LockdownClient + + properties *DeviceProperties + + lockdown *lockdown + imageMounter ImageMounter + screenshot Screenshot + simulateLocation SimulateLocation + installationProxy InstallationProxy + instruments Instruments + afc Afc + houseArrest HouseArrest + syslogRelay SyslogRelay + diagnosticsRelay DiagnosticsRelay + springBoard SpringBoard + crashReportMover CrashReportMover + pcapd Pcapd + perfd []Perfd +} + +func (d *device) Properties() DeviceProperties { + return *d.properties +} + +func (d *device) NewConnect(port int, timeout ...time.Duration) (InnerConn, error) { + newClient, err := libimobiledevice.NewUsbmuxClient(timeout...) + if err != nil { + return nil, err + } + + var pkt libimobiledevice.Packet + if pkt, err = newClient.NewPlistPacket( + newClient.NewConnectRequest(d.properties.DeviceID, port), + ); err != nil { + newClient.Close() + return nil, err + } + + if err = newClient.SendPacket(pkt); err != nil { + newClient.Close() + return nil, err + } + + if _, err = newClient.ReceivePacket(); err != nil { + newClient.Close() + return nil, err + } + + return newClient.InnerConn(), err +} + +func (d *device) ReadPairRecord() (pairRecord *PairRecord, err error) { + var pkt libimobiledevice.Packet + if pkt, err = d.umClient.NewPlistPacket( + d.umClient.NewReadPairRecordRequest(d.properties.SerialNumber), + ); err != nil { + return nil, err + } + + if err = d.umClient.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = d.umClient.ReceivePacket(); err != nil { + return nil, err + } + + reply := struct { + Data []byte `plist:"PairRecordData"` + }{} + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + + var record PairRecord + if _, err = plist.Unmarshal(reply.Data, &record); err != nil { + return nil, err + } + + pairRecord = &record + return +} + +func (d *device) SavePairRecord(pairRecord *PairRecord) (err error) { + var data []byte + if data, err = plist.Marshal(pairRecord, plist.XMLFormat); err != nil { + return err + } + + var pkt libimobiledevice.Packet + if pkt, err = d.umClient.NewPlistPacket( + d.umClient.NewSavePairRecordRequest(d.properties.SerialNumber, d.properties.DeviceID, data), + ); err != nil { + return err + } + + if err = d.umClient.SendPacket(pkt); err != nil { + return err + } + + if _, err = d.umClient.ReceivePacket(); err != nil { + return err + } + + return +} + +func (d *device) DeletePairRecord() (err error) { + var pkt libimobiledevice.Packet + if pkt, err = d.umClient.NewPlistPacket( + d.umClient.NewDeletePairRecordRequest(d.properties.SerialNumber), + ); err != nil { + return err + } + + if err = d.umClient.SendPacket(pkt); err != nil { + return err + } + + if _, err = d.umClient.ReceivePacket(); err != nil { + return err + } + + return +} + +func (d *device) lockdownService() (lockdown Lockdown, err error) { + // if d.lockdown != nil { + // return d.lockdown, nil + // } + + var innerConn InnerConn + if innerConn, err = d.NewConnect(LockdownPort, 0); err != nil { + return nil, err + } + d.lockdownClient = libimobiledevice.NewLockdownClient(innerConn) + d.lockdown = newLockdown(d) + _, err = d.lockdown._getProductVersion() + lockdown = d.lockdown + return +} + +func (d *device) QueryType() (LockdownType, error) { + if _, err := d.lockdownService(); err != nil { + return LockdownType{}, err + } + return d.lockdown.QueryType() +} + +func (d *device) GetValue(domain, key string) (v interface{}, err error) { + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.lockdown.pairRecord == nil { + if err = d.lockdown.handshake(); err != nil { + return nil, err + } + } + if err = d.lockdown.startSession(d.lockdown.pairRecord); err != nil { + return nil, err + } + if v, err = d.lockdown.GetValue(domain, key); err != nil { + return nil, err + } + err = d.lockdown.stopSession() + return +} + +func (d *device) Pair() (pairRecord *PairRecord, err error) { + if _, err = d.lockdownService(); err != nil { + return nil, err + } + return d.lockdown.Pair() +} + +func (d *device) imageMounterService() (imageMounter ImageMounter, err error) { + if d.imageMounter != nil { + return d.imageMounter, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.imageMounter, err = d.lockdown.ImageMounterService(); err != nil { + return nil, err + } + imageMounter = d.imageMounter + return +} + +func (d *device) Images(imgType ...string) (imageSignatures [][]byte, err error) { + if _, err = d.imageMounterService(); err != nil { + return nil, err + } + if len(imgType) == 0 { + imgType = []string{"Developer"} + } + return d.imageMounter.Images(imgType[0]) +} + +func (d *device) MountDeveloperDiskImage(dmgPath string, signaturePath string) (err error) { + if _, err = d.imageMounterService(); err != nil { + return err + } + devImgPath := "/private/var/mobile/Media/PublicStaging/staging.dimage" + return d.imageMounter.UploadImageAndMount("Developer", devImgPath, dmgPath, signaturePath) +} + +func (d *device) screenshotService() (screenshot Screenshot, err error) { + if d.screenshot != nil { + return d.screenshot, nil + } + + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.screenshot, err = d.lockdown.ScreenshotService(); err != nil { + return nil, err + } + screenshot = d.screenshot + return +} + +func (d *device) Screenshot() (raw *bytes.Buffer, err error) { + if _, err = d.screenshotService(); err != nil { + return nil, err + } + return d.screenshot.Take() +} + +func (d *device) simulateLocationService() (simulateLocation SimulateLocation, err error) { + if d.simulateLocation != nil { + return d.simulateLocation, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.simulateLocation, err = d.lockdown.SimulateLocationService(); err != nil { + return nil, err + } + simulateLocation = d.simulateLocation + return +} + +func (d *device) SimulateLocationUpdate(longitude float64, latitude float64, coordinateSystem ...CoordinateSystem) (err error) { + if _, err = d.simulateLocationService(); err != nil { + return err + } + return d.simulateLocation.Update(longitude, latitude, coordinateSystem...) +} + +func (d *device) SimulateLocationRecover() (err error) { + if _, err = d.simulateLocationService(); err != nil { + return err + } + return d.simulateLocation.Recover() +} + +func (d *device) installationProxyService() (installationProxy InstallationProxy, err error) { + if d.installationProxy != nil { + return d.installationProxy, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.installationProxy, err = d.lockdown.InstallationProxyService(); err != nil { + return nil, err + } + installationProxy = d.installationProxy + return +} + +func (d *device) InstallationProxyBrowse(opts ...InstallationProxyOption) (currentList []interface{}, err error) { + if _, err = d.installationProxyService(); err != nil { + return nil, err + } + return d.installationProxy.Browse(opts...) +} + +func (d *device) InstallationProxyLookup(opts ...InstallationProxyOption) (lookupResult interface{}, err error) { + if _, err = d.installationProxyService(); err != nil { + return nil, err + } + return d.installationProxy.Lookup(opts...) +} + +func (d *device) newInstrumentsService() (instruments Instruments, err error) { + // NOTICE: each instruments service should have individual connection, otherwise it will be blocked + if _, err = d.lockdownService(); err != nil { + return + } + return d.lockdown.InstrumentsService() +} + +func (d *device) instrumentsService() (instruments Instruments, err error) { + if d.instruments != nil { + return d.instruments, nil + } + if d.instruments, err = d.newInstrumentsService(); err != nil { + return nil, err + } + instruments = d.instruments + return +} + +func (d *device) AppLaunch(bundleID string, opts ...AppLaunchOption) (pid int, err error) { + if _, err = d.instrumentsService(); err != nil { + return 0, err + } + return d.instruments.AppLaunch(bundleID, opts...) +} + +func (d *device) AppKill(pid int) (err error) { + if _, err = d.instrumentsService(); err != nil { + return err + } + return d.instruments.AppKill(pid) +} + +func (d *device) AppRunningProcesses() (processes []Process, err error) { + if _, err = d.instrumentsService(); err != nil { + return nil, err + } + return d.instruments.AppRunningProcesses() +} + +func (d *device) AppList(opts ...AppListOption) (apps []Application, err error) { + if _, err = d.instrumentsService(); err != nil { + return nil, err + } + return d.instruments.AppList(opts...) +} + +func (d *device) DeviceInfo() (devInfo *DeviceInfo, err error) { + if _, err = d.instrumentsService(); err != nil { + return nil, err + } + return d.instruments.DeviceInfo() +} + +func (d *device) testmanagerdService() (testmanagerd Testmanagerd, err error) { + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if testmanagerd, err = d.lockdown.TestmanagerdService(); err != nil { + return nil, err + } + return +} + +func (d *device) AfcService() (afc Afc, err error) { + if d.afc != nil { + return d.afc, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.afc, err = d.lockdown.AfcService(); err != nil { + return nil, err + } + afc = d.afc + return +} + +func (d *device) AppInstall(ipaPath string) (err error) { + if _, err = d.AfcService(); err != nil { + return err + } + + stagingPath := "PublicStaging" + if _, err = d.afc.Stat(stagingPath); err != nil { + if err != ErrAfcStatNotExist { + return err + } + if err = d.afc.Mkdir(stagingPath); err != nil { + return fmt.Errorf("app install: %w", err) + } + } + + var info map[string]interface{} + if info, err = ipa.Info(ipaPath); err != nil { + return err + } + bundleID, ok := info["CFBundleIdentifier"] + if !ok { + return errors.New("can't find 'CFBundleIdentifier'") + } + + installationPath := path.Join(stagingPath, fmt.Sprintf("%s.ipa", bundleID)) + + var data []byte + if data, err = os.ReadFile(ipaPath); err != nil { + return err + } + if err = d.afc.WriteFile(installationPath, data, AfcFileModeWr); err != nil { + return err + } + + if _, err = d.installationProxyService(); err != nil { + return err + } + + return d.installationProxy.Install(fmt.Sprintf("%s", bundleID), installationPath) +} + +func (d *device) AppUninstall(bundleID string) (err error) { + if _, err = d.installationProxyService(); err != nil { + return err + } + + return d.installationProxy.Uninstall(bundleID) +} + +func (d *device) HouseArrestService() (houseArrest HouseArrest, err error) { + if d.houseArrest != nil { + return d.houseArrest, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.houseArrest, err = d.lockdown.HouseArrestService(); err != nil { + return nil, err + } + houseArrest = d.houseArrest + return +} + +func (d *device) syslogRelayService() (syslogRelay SyslogRelay, err error) { + if d.syslogRelay != nil { + return d.syslogRelay, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.syslogRelay, err = d.lockdown.SyslogRelayService(); err != nil { + return nil, err + } + syslogRelay = d.syslogRelay + return +} + +func (d *device) Syslog() (lines <-chan string, err error) { + if _, err = d.syslogRelayService(); err != nil { + return nil, err + } + return d.syslogRelay.Lines(), nil +} + +func (d *device) SyslogStop() { + if d.syslogRelay == nil { + return + } + d.syslogRelay.Stop() +} + +func (d *device) Reboot() (err error) { + if _, err = d.lockdownService(); err != nil { + return + } + if d.diagnosticsRelay, err = d.lockdown.DiagnosticsRelayService(); err != nil { + return + } + if err = d.diagnosticsRelay.Reboot(); err != nil { + return + } + return +} + +func (d *device) Shutdown() (err error) { + if _, err = d.lockdownService(); err != nil { + return + } + if d.diagnosticsRelay, err = d.lockdown.DiagnosticsRelayService(); err != nil { + return + } + if err = d.diagnosticsRelay.Shutdown(); err != nil { + return + } + return +} + +func (d *device) springBoardService() (springBoard SpringBoard, err error) { + if d.springBoard != nil { + return d.springBoard, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.springBoard, err = d.lockdown.SpringBoardService(); err != nil { + return nil, err + } + springBoard = d.springBoard + return +} + +func (d *device) GetIconPNGData(bundleId string) (raw *bytes.Buffer, err error) { + if _, err = d.lockdownService(); err != nil { + return + } + if d.springBoard, err = d.lockdown.SpringBoardService(); err != nil { + return + } + if raw, err = d.springBoard.GetIconPNGData(bundleId); err != nil { + return + } + return +} + +func (d *device) GetInterfaceOrientation() (orientation libimobiledevice.OrientationState, err error) { + if _, err = d.springBoardService(); err != nil { + return + } + if orientation, err = d.springBoard.GetInterfaceOrientation(); err != nil { + return + } + return +} + +func (d *device) PcapdService() (pcapd Pcapd, err error) { + // if d.pcapd != nil { + // return d.pcapd, nil + // } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.pcapd, err = d.lockdown.PcapdService(); err != nil { + return nil, err + } + pcapd = d.pcapd + return +} + +func (d *device) Pcap() (lines <-chan []byte, err error) { + if _, err = d.PcapdService(); err != nil { + return nil, err + } + return d.pcapd.Packet(), nil +} + +func (d *device) PcapStop() { + if d.pcapd == nil { + return + } + d.pcapd.Stop() +} + +func (d *device) PerfStart(opts ...PerfOption) (data <-chan []byte, err error) { + perfOptions := defaulPerfOption() + for _, fn := range opts { + fn(perfOptions) + } + + // wait until get pid for bundle id + if perfOptions.BundleID != "" { + instruments, err := d.newInstrumentsService() + if err != nil { + fmt.Printf("get pid by bundle id failed: %v\n", err) + os.Exit(1) + } + + for { + pid, err := instruments.getPidByBundleID(perfOptions.BundleID) + if err != nil { + time.Sleep(1 * time.Second) + continue + } + perfOptions.Pid = pid + break + } + } + + // processAttributes must contain pid, or it can't get process info, reason unknown + if !containString(perfOptions.ProcessAttributes, "pid") { + perfOptions.ProcessAttributes = append(perfOptions.ProcessAttributes, "pid") + } + + outCh := make(chan []byte, 100) + + if perfOptions.SysCPU || perfOptions.SysMem || perfOptions.SysDisk || + perfOptions.SysNetwork { + perfd, err := d.newPerfdSysmontap(perfOptions) + if err != nil { + return nil, err + } + data, err := perfd.Start() + if err != nil { + return nil, err + } + go func() { + for { + outCh <- (<-data) + } + }() + d.perfd = append(d.perfd, perfd) + } + + if perfOptions.Network { + perfd, err := d.newPerfdNetworking(perfOptions) + if err != nil { + return nil, err + } + data, err := perfd.Start() + if err != nil { + return nil, err + } + go func() { + for { + outCh <- (<-data) + } + }() + d.perfd = append(d.perfd, perfd) + } + + if perfOptions.FPS || perfOptions.gpu { + perfd, err := d.newPerfdGraphicsOpengl(perfOptions) + if err != nil { + return nil, err + } + data, err := perfd.Start() + if err != nil { + return nil, err + } + go func() { + for { + outCh <- (<-data) + } + }() + d.perfd = append(d.perfd, perfd) + } + + return outCh, nil +} + +func (d *device) PerfStop() { + if d.perfd == nil { + return + } + for _, p := range d.perfd { + p.Stop() + } +} + +func (d *device) crashReportMoverService() (crashReportMover CrashReportMover, err error) { + if d.crashReportMover != nil { + return d.crashReportMover, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.crashReportMover, err = d.lockdown.CrashReportMoverService(); err != nil { + return nil, err + } + crashReportMover = d.crashReportMover + return +} + +func (d *device) MoveCrashReport(hostDir string, opts ...CrashReportMoverOption) (err error) { + if _, err = d.crashReportMoverService(); err != nil { + return err + } + return d.crashReportMover.Move(hostDir, opts...) +} + +func (d *device) XCTest(bundleID string, opts ...XCTestOption) (out <-chan string, cancel context.CancelFunc, err error) { + xcTestOpt := defaultXCTestOption() + for _, fn := range opts { + fn(xcTestOpt) + } + + ctx, cancelFunc := context.WithCancel(context.TODO()) + _out := make(chan string) + + xcodeVersion := uint64(30) + + var tmSrv1 Testmanagerd + if tmSrv1, err = d.testmanagerdService(); err != nil { + return _out, cancelFunc, err + } + + var xcTestManager1 XCTestManagerDaemon + if xcTestManager1, err = tmSrv1.newXCTestManagerDaemon(); err != nil { + return _out, cancelFunc, err + } + + var version []int + if version, err = d.lockdown._getProductVersion(); err != nil { + return _out, cancelFunc, err + } + + if DeviceVersion(version...) >= DeviceVersion(11, 0, 0) { + if err = xcTestManager1.initiateControlSession(xcodeVersion); err != nil { + return _out, cancelFunc, err + } + } + + var tmSrv2 Testmanagerd + if tmSrv2, err = d.testmanagerdService(); err != nil { + return _out, cancelFunc, err + } + + var xcTestManager2 XCTestManagerDaemon + if xcTestManager2, err = tmSrv2.newXCTestManagerDaemon(); err != nil { + return _out, cancelFunc, err + } + + xcTestManager2.registerCallback("_XCT_logDebugMessage:", func(m libimobiledevice.DTXMessageResult) { + // more information ( each operation ) + // fmt.Println("###### xcTestManager2 ### -->", m) + if strings.Contains(fmt.Sprintf("%s", m), "Received test runner ready reply with error: (null)") { + // fmt.Println("###### xcTestManager2 ### -->", fmt.Sprintf("%v", m.Aux[0])) + time.Sleep(time.Second) + if err = xcTestManager2.startExecutingTestPlan(xcodeVersion); err != nil { + debugLog(fmt.Sprintf("startExecutingTestPlan %d: %s", xcodeVersion, err)) + return + } + } + }) + xcTestManager2.registerCallback("_Golang-iDevice_Unregistered", func(m libimobiledevice.DTXMessageResult) { + // more information + // _XCT_testRunnerReadyWithCapabilities: + // _XCT_didBeginExecutingTestPlan + // _XCT_didBeginInitializingForUITesting + // _XCT_testSuite:didStartAt: + // _XCT_testCase:method:willStartActivity: + // _XCT_testCase:method:didFinishActivity: + // _XCT_testCaseDidStartForTestClass:method: + // fmt.Println("###### xcTestManager2 ### _Unregistered -->", m) + }) + + sessionId := uuid.NewV4() + if err = xcTestManager2.initiateSession(xcodeVersion, nskeyedarchiver.NewNSUUID(sessionId.Bytes())); err != nil { + return _out, cancelFunc, err + } + + if _, err = d.installationProxyService(); err != nil { + return _out, cancelFunc, err + } + + var vResult interface{} + if vResult, err = d.installationProxy.Lookup(WithBundleIDs(bundleID)); err != nil { + return _out, cancelFunc, err + } + + lookupResult := vResult.(map[string]interface{}) + lookupResult = lookupResult[bundleID].(map[string]interface{}) + appContainer := lookupResult["Container"].(string) + appPath := lookupResult["Path"].(string) + + var pathXCTestCfg string + if pathXCTestCfg, err = d._uploadXCTestConfiguration(bundleID, sessionId, lookupResult); err != nil { + return _out, cancelFunc, err + } + + if _, err = d.instrumentsService(); err != nil { + return _out, cancelFunc, err + } + + if err = d.instruments.appProcess(bundleID); err != nil { + return _out, cancelFunc, err + } + + pathXCTestConfiguration := appContainer + pathXCTestCfg + + appEnv := map[string]interface{}{ + "CA_ASSERT_MAIN_THREAD_TRANSACTIONS": "0", + "CA_DEBUG_TRANSACTIONS": "0", + "DYLD_FRAMEWORK_PATH": appPath + "/Frameworks:", + "DYLD_LIBRARY_PATH": appPath + "/Frameworks", + "NSUnbufferedIO": "YES", + "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", + "WDA_PRODUCT_BUNDLE_IDENTIFIER": "", + "XCTestConfigurationFilePath": pathXCTestConfiguration, // Running tests with active test configuration: + // "XCTestBundlePath": fmt.Sprintf("%s/PlugIns/%s.xctest", appPath, name), // !!! ERROR + // "XCTestSessionIdentifier": sessionId.String(), // !!! ERROR + // "XCTestSessionIdentifier": "", + "XCODE_DBG_XPC_EXCLUSIONS": "com.apple.dt.xctestSymbolicator", + "MJPEG_SERVER_PORT": "", + "USE_PORT": "", + "LLVM_PROFILE_FILE": appContainer + "/tmp/%p.profraw", + } + if DeviceVersion(version...) >= DeviceVersion(11, 0, 0) { + appEnv["DYLD_INSERT_LIBRARIES"] = "/Developer/usr/lib/libMainThreadChecker.dylib" + appEnv["OS_ACTIVITY_DT_MODE"] = "YES" + } + appArgs := []interface{}{ + "-NSTreatUnknownArgumentsAsOpen", "NO", + "-ApplePersistenceIgnoreState", "YES", + } + appOpt := map[string]interface{}{ + "StartSuspendedKey": uint64(0), + } + if DeviceVersion(version...) >= DeviceVersion(12, 0, 0) { + appOpt["ActivateSuspended"] = uint64(1) + } + + if len(xcTestOpt.appEnv) != 0 { + for k, v := range xcTestOpt.appEnv { + appEnv[k] = v + } + } + + if len(xcTestOpt.appOpt) != 0 { + for k, v := range xcTestOpt.appEnv { + appOpt[k] = v + } + } + + d.instruments.registerCallback("outputReceived:fromProcess:atTime:", func(m libimobiledevice.DTXMessageResult) { + // fmt.Println("###### instruments ### -->", m.Aux[0]) + _out <- fmt.Sprintf("%s", m.Aux[0]) + }) + + var pid int + if pid, err = d.instruments.AppLaunch(bundleID, + WithAppPath(appPath), + WithEnvironment(appEnv), + WithArguments(appArgs), + WithOptions(appOpt), + WithKillExisting(true), + ); err != nil { + return _out, cancelFunc, err + } + + // see https://github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/issues/31 + // if err = d.instruments.startObserving(pid); err != nil { + // return _out, cancelFunc, err + // } + + if DeviceVersion(version...) >= DeviceVersion(12, 0, 0) { + err = xcTestManager1.authorizeTestSession(pid) + } else if DeviceVersion(version...) <= DeviceVersion(9, 0, 0) { + err = xcTestManager1.initiateControlSessionForTestProcessID(pid) + } else { + err = xcTestManager1.initiateControlSessionForTestProcessIDProtocolVersion(pid, xcodeVersion) + } + if err != nil { + return _out, cancelFunc, err + } + + go func() { + d.instruments.registerCallback("_Golang-iDevice_Over", func(_ libimobiledevice.DTXMessageResult) { + cancelFunc() + }) + + <-ctx.Done() + tmSrv1.close() + tmSrv2.close() + xcTestManager1.close() + xcTestManager2.close() + if _err := d.AppKill(pid); _err != nil { + debugLog(fmt.Sprintf("xctest kill: %d", pid)) + } + // time.Sleep(time.Second) + close(_out) + }() + + return _out, cancelFunc, err +} + +func (d *device) _uploadXCTestConfiguration(bundleID string, sessionId uuid.UUID, lookupResult map[string]interface{}) (pathXCTestCfg string, err error) { + if _, err = d.HouseArrestService(); err != nil { + return "", err + } + + var appAfc Afc + if appAfc, err = d.houseArrest.Container(bundleID); err != nil { + return "", err + } + + appTmpFilenames, err := appAfc.ReadDir("/tmp") + if err != nil { + return "", err + } + + for _, tName := range appTmpFilenames { + if strings.HasSuffix(tName, ".xctestconfiguration") { + if _err := appAfc.Remove(fmt.Sprintf("/tmp/%s", tName)); _err != nil { + debugLog(fmt.Sprintf("remove /tmp/%s: %s", tName, err)) + continue + } + } + } + + nameExec := lookupResult["CFBundleExecutable"].(string) + name := nameExec[:len(nameExec)-len("-Runner")] + appPath := lookupResult["Path"].(string) + + pathXCTestCfg = fmt.Sprintf("/tmp/%s-%s.xctestconfiguration", name, strings.ToUpper(sessionId.String())) + + var content []byte + if content, err = nskeyedarchiver.Marshal( + nskeyedarchiver.NewXCTestConfiguration( + nskeyedarchiver.NewNSUUID(sessionId.Bytes()), + nskeyedarchiver.NewNSURL(fmt.Sprintf("%s/PlugIns/%s.xctest", appPath, name)), + bundleID, + appPath, + ), + ); err != nil { + return "", err + } + + if err = appAfc.WriteFile(pathXCTestCfg, content, AfcFileModeWr); err != nil { + return "", err + } + + return +} diff --git a/hrp/pkg/gidevice/device_test.go b/hrp/pkg/gidevice/device_test.go new file mode 100644 index 00000000..ad11b9ed --- /dev/null +++ b/hrp/pkg/gidevice/device_test.go @@ -0,0 +1,174 @@ +package gidevice + +import ( + "fmt" + "os" + "os/signal" + "testing" + "time" +) + +var dev Device + +func setupDevice(t *testing.T) { + setupUsbmux(t) + devices, err := um.Devices() + if err != nil { + t.Fatal(err) + } + + if len(devices) == 0 { + t.Fatal("No Device") + } + + dev = devices[0] +} +func Test_device_ReadPairRecord(t *testing.T) { + setupDevice(t) + + pairRecord, err := dev.ReadPairRecord() + if err != nil { + t.Fatal(err) + } + + t.Log(pairRecord.HostID, pairRecord.SystemBUID, pairRecord.WiFiMACAddress) +} + +func Test_device_NewConnect(t *testing.T) { + setupDevice(t) + + if _, err := dev.NewConnect(LockdownPort); err != nil { + t.Fatal(err) + } +} + +func Test_device_DeletePairRecord(t *testing.T) { + setupDevice(t) + + if err := dev.DeletePairRecord(); err != nil { + t.Fatal(err) + } + +} + +func Test_device_SavePairRecord(t *testing.T) { + setupLockdownSrv(t) + + pairRecord, err := lockdownSrv.Pair() + if err != nil { + t.Fatal(err) + } + + err = dev.SavePairRecord(pairRecord) + if err != nil { + t.Fatal(err) + } +} + +func Test_device_XCTest(t *testing.T) { + setupLockdownSrv(t) + + bundleID = "com.leixipaopao.WebDriverAgentRunner.xctrunner" + out, cancel, err := dev.XCTest(bundleID) + // out, cancel, err := dev.XCTest(bundleID, WithXCTestEnv(map[string]interface{}{"USE_PORT": 8222, "MJPEG_SERVER_PORT": 8333})) + if err != nil { + t.Fatal(err) + } + + done := make(chan os.Signal, 1) + signal.Notify(done, os.Interrupt) + + go func() { + for s := range out { + fmt.Print(s) + } + done <- os.Interrupt + }() + + for { + select { + case <-done: + cancel() + fmt.Println() + t.Log("DONE") + return + } + } +} + +func Test_device_AppInstall(t *testing.T) { + setupLockdownSrv(t) + + ipaPath := "/private/tmp/derivedDataPath/Build/Products/Release-iphoneos/WebDriverAgentRunner-Runner.ipa" + err := dev.AppInstall(ipaPath) + if err != nil { + t.Fatal(err) + } +} + +func Test_device_AppUninstall(t *testing.T) { + setupLockdownSrv(t) + + bundleID = "com.leixipaopao.WebDriverAgentRunner.xctrunner" + err := dev.AppUninstall(bundleID) + if err != nil { + t.Fatal(err) + } +} + +func Test_device_Syslog(t *testing.T) { + setupLockdownSrv(t) + + dev.SyslogStop() + + lines, err := dev.Syslog() + if err != nil { + t.Fatal(err) + } + + done := make(chan os.Signal, 1) + + go func() { + for line := range lines { + fmt.Println(line) + } + done <- os.Interrupt + t.Log("DONE!!!") + }() + + signal.Notify(done, os.Interrupt, os.Kill) + + // <-done + time.Sleep(3 * time.Second) + dev.SyslogStop() + time.Sleep(200 * time.Millisecond) +} + +func Test_device_Reboot(t *testing.T) { + setupDevice(t) + dev.Reboot() +} + +func Test_device_Shutdown(t *testing.T) { + setupDevice(t) + dev.Shutdown() +} + +func Test_device_InstallationProxyBrowse(t *testing.T) { + setupDevice(t) + + list, err := dev.InstallationProxyBrowse( + WithApplicationType(ApplicationTypeUser), + WithReturnAttributes("CFBundleDisplayName", "CFBundleIdentifier", "SequenceNumber", "SequenceNumber"), + ) + // list, err := dev.InstallationProxyBrowse() + if err != nil { + t.Fatal(err) + } + + t.Log(len(list)) + + for _, l := range list { + t.Logf("%#v", l) + } +} diff --git a/hrp/pkg/gidevice/diagnosticsrelay.go b/hrp/pkg/gidevice/diagnosticsrelay.go new file mode 100644 index 00000000..6f2f0544 --- /dev/null +++ b/hrp/pkg/gidevice/diagnosticsrelay.go @@ -0,0 +1,39 @@ +package gidevice + +import "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" + +func newDiagnosticsRelay(client *libimobiledevice.DiagnosticsRelayClient) *diagnostics { + return &diagnostics{ + client: client, + } +} + +type diagnostics struct { + client *libimobiledevice.DiagnosticsRelayClient +} + +func (d *diagnostics) Reboot() (err error) { + var pkt libimobiledevice.Packet + if pkt, err = d.client.NewXmlPacket( + d.client.NewBasicRequest("Restart"), + ); err != nil { + return + } + if err = d.client.SendPacket(pkt); err != nil { + return err + } + return +} + +func (d *diagnostics) Shutdown() (err error) { + var pkt libimobiledevice.Packet + if pkt, err = d.client.NewXmlPacket( + d.client.NewBasicRequest("Shutdown"), + ); err != nil { + return + } + if err = d.client.SendPacket(pkt); err != nil { + return err + } + return +} diff --git a/hrp/pkg/gidevice/housearrest.go b/hrp/pkg/gidevice/housearrest.go new file mode 100644 index 00000000..33e552b1 --- /dev/null +++ b/hrp/pkg/gidevice/housearrest.go @@ -0,0 +1,57 @@ +package gidevice + +import "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" + +var _ HouseArrest = (*houseArrest)(nil) + +func newHouseArrest(client *libimobiledevice.HouseArrestClient) *houseArrest { + return &houseArrest{ + client: client, + } +} + +type houseArrest struct { + client *libimobiledevice.HouseArrestClient +} + +func (h *houseArrest) Documents(bundleID string) (afc Afc, err error) { + var pkt libimobiledevice.Packet + if pkt, err = h.client.NewXmlPacket( + h.client.NewDocumentsRequest(bundleID), + ); err != nil { + return nil, err + } + + if err = h.client.SendPacket(pkt); err != nil { + return nil, err + } + + if _, err = h.client.ReceivePacket(); err != nil { + return nil, err + } + + afcClient := libimobiledevice.NewAfcClient(h.client.InnerConn()) + afc = newAfc(afcClient) + return +} + +func (h *houseArrest) Container(bundleID string) (afc Afc, err error) { + var pkt libimobiledevice.Packet + if pkt, err = h.client.NewXmlPacket( + h.client.NewContainerRequest(bundleID), + ); err != nil { + return nil, err + } + + if err = h.client.SendPacket(pkt); err != nil { + return nil, err + } + + if _, err = h.client.ReceivePacket(); err != nil { + return nil, err + } + + afcClient := libimobiledevice.NewAfcClient(h.client.InnerConn()) + afc = newAfc(afcClient) + return +} diff --git a/hrp/pkg/gidevice/housearrest_test.go b/hrp/pkg/gidevice/housearrest_test.go new file mode 100644 index 00000000..6c69bcc3 --- /dev/null +++ b/hrp/pkg/gidevice/housearrest_test.go @@ -0,0 +1,58 @@ +package gidevice + +import ( + "testing" +) + +var houseArrestSrv HouseArrest + +func setupHouseArrestSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if houseArrestSrv, err = lockdownSrv.HouseArrestService(); err != nil { + t.Fatal(err) + } +} + +func Test_houseArrest_Documents(t *testing.T) { + setupHouseArrestSrv(t) + + bundleID = "com.apple.iMovie" + appAfc, err := houseArrestSrv.Documents(bundleID) + if err != nil { + t.Fatal(err) + } + + names, err := appAfc.ReadDir("Documents") + if err != nil { + t.Fatal(err) + } + + for _, name := range names { + t.Log(name) + } +} + +func Test_houseArrest_Container(t *testing.T) { + setupHouseArrestSrv(t) + + bundleID = "com.apple.iMovie" + appAfc, err := houseArrestSrv.Documents(bundleID) + if err != nil { + t.Fatal(err) + } + + names, err := appAfc.ReadDir("Documents") + if err != nil { + t.Fatal(err) + } + + for _, name := range names { + t.Log(name) + } +} diff --git a/hrp/pkg/gidevice/idevice.go b/hrp/pkg/gidevice/idevice.go new file mode 100644 index 00000000..7398ff5b --- /dev/null +++ b/hrp/pkg/gidevice/idevice.go @@ -0,0 +1,482 @@ +package gidevice + +import ( + "bytes" + "context" + "fmt" + "time" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/nskeyedarchiver" +) + +type Usbmux interface { + Devices() ([]Device, error) + ReadBUID() (string, error) + Listen(chan Device) (context.CancelFunc, error) +} + +type Device interface { + Properties() DeviceProperties + + NewConnect(port int, timeout ...time.Duration) (InnerConn, error) + ReadPairRecord() (pairRecord *PairRecord, err error) + SavePairRecord(pairRecord *PairRecord) (err error) + DeletePairRecord() (err error) + + lockdownService() (lockdown Lockdown, err error) + QueryType() (LockdownType, error) + GetValue(domain, key string) (v interface{}, err error) + Pair() (pairRecord *PairRecord, err error) + + imageMounterService() (imageMounter ImageMounter, err error) + Images(imgType ...string) (imageSignatures [][]byte, err error) + MountDeveloperDiskImage(dmgPath string, signaturePath string) (err error) + + screenshotService() (lockdown Screenshot, err error) + Screenshot() (raw *bytes.Buffer, err error) + + simulateLocationService() (simulateLocation SimulateLocation, err error) + SimulateLocationUpdate(longitude float64, latitude float64, coordinateSystem ...CoordinateSystem) (err error) + SimulateLocationRecover() (err error) + + installationProxyService() (installationProxy InstallationProxy, err error) + InstallationProxyBrowse(opts ...InstallationProxyOption) (currentList []interface{}, err error) + InstallationProxyLookup(opts ...InstallationProxyOption) (lookupResult interface{}, err error) + + instrumentsService() (instruments Instruments, err error) + AppLaunch(bundleID string, opts ...AppLaunchOption) (pid int, err error) + AppKill(pid int) (err error) + AppRunningProcesses() (processes []Process, err error) + AppList(opts ...AppListOption) (apps []Application, err error) + DeviceInfo() (devInfo *DeviceInfo, err error) + + AfcService() (afc Afc, err error) + AppInstall(ipaPath string) (err error) + AppUninstall(bundleID string) (err error) + + HouseArrestService() (houseArrest HouseArrest, err error) + + syslogRelayService() (syslogRelay SyslogRelay, err error) + Syslog() (lines <-chan string, err error) + SyslogStop() + + PcapdService() (pcapd Pcapd, err error) + Pcap() (packet <-chan []byte, err error) + PcapStop() + + Reboot() error + Shutdown() error + + crashReportMoverService() (crashReportMover CrashReportMover, err error) + MoveCrashReport(hostDir string, opts ...CrashReportMoverOption) (err error) + + XCTest(bundleID string, opts ...XCTestOption) (out <-chan string, cancel context.CancelFunc, err error) + + springBoardService() (springBoard SpringBoard, err error) + GetIconPNGData(bundleId string) (raw *bytes.Buffer, err error) + GetInterfaceOrientation() (orientation OrientationState, err error) + + PerfStart(opts ...PerfOption) (data <-chan []byte, err error) + PerfStop() +} + +type DeviceProperties = libimobiledevice.DeviceProperties + +type OrientationState = libimobiledevice.OrientationState + +type Lockdown interface { + QueryType() (LockdownType, error) + GetValue(domain, key string) (v interface{}, err error) + SetValue(domain, key string, value interface{}) (err error) + Pair() (pairRecord *PairRecord, err error) + EnterRecovery() (err error) + + handshake() (err error) + + startSession(pairRecord *PairRecord) (err error) + stopSession() (err error) + startService(service string, escrowBag []byte) (dynamicPort int, enableSSL bool, err error) + + ImageMounterService() (imageMounter ImageMounter, err error) + ScreenshotService() (screenshot Screenshot, err error) + SimulateLocationService() (simulateLocation SimulateLocation, err error) + InstallationProxyService() (installationProxy InstallationProxy, err error) + InstrumentsService() (instruments Instruments, err error) + TestmanagerdService() (testmanagerd Testmanagerd, err error) + AfcService() (afc Afc, err error) + HouseArrestService() (houseArrest HouseArrest, err error) + SyslogRelayService() (syslogRelay SyslogRelay, err error) + DiagnosticsRelayService() (diagnostics DiagnosticsRelay, err error) + CrashReportMoverService() (crashReportMover CrashReportMover, err error) + SpringBoardService() (springBoard SpringBoard, err error) +} + +type ImageMounter interface { + Images(imgType string) (imageSignatures [][]byte, err error) + UploadImage(imgType, dmgPath string, signatureData []byte) (err error) + Mount(imgType, devImgPath string, signatureData []byte) (err error) + + UploadImageAndMount(imgType, devImgPath, dmgPath, signaturePath string) (err error) +} + +type Screenshot interface { + exchange() (err error) + Take() (raw *bytes.Buffer, err error) +} + +type SimulateLocation interface { + Update(longitude float64, latitude float64, coordinateSystem ...CoordinateSystem) (err error) + // Recover try to revert back + Recover() (err error) +} + +type InstallationProxy interface { + Browse(opts ...InstallationProxyOption) (currentList []interface{}, err error) + Lookup(opts ...InstallationProxyOption) (lookupResult interface{}, err error) + Install(bundleID, packagePath string) (err error) + Uninstall(bundleID string) (err error) +} + +type Instruments interface { + AppLaunch(bundleID string, opts ...AppLaunchOption) (pid int, err error) + AppKill(pid int) (err error) + AppRunningProcesses() (processes []Process, err error) + AppList(opts ...AppListOption) (apps []Application, err error) + DeviceInfo() (devInfo *DeviceInfo, err error) + + getPidByBundleID(bundleID string) (pid int, err error) + appProcess(bundleID string) (err error) + startObserving(pid int) (err error) + + notifyOfPublishedCapabilities() (err error) + requestChannel(channel string) (id uint32, err error) + call(channel, selector string, auxiliaries ...interface{}) (result *libimobiledevice.DTXMessageResult, err error) + + // sysMonSetConfig(cfg ...interface{}) (err error) + // SysMonStart(cfg ...interface{}) (_ interface{}, err error) + + registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) +} + +type Testmanagerd interface { + notifyOfPublishedCapabilities() (err error) + requestChannel(channel string) (id uint32, err error) + newXCTestManagerDaemon() (xcTestManager XCTestManagerDaemon, err error) + + invoke(selector string, args *libimobiledevice.AuxBuffer, channel uint32, expectsReply bool) (*libimobiledevice.DTXMessageResult, error) + + registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) + close() +} + +type Afc interface { + DiskInfo() (diskInfo *AfcDiskInfo, err error) + ReadDir(dirname string) (names []string, err error) + Stat(filename string) (info *AfcFileInfo, err error) + Open(filename string, mode AfcFileMode) (file *AfcFile, err error) + Remove(filePath string) (err error) + Rename(oldPath string, newPath string) (err error) + Mkdir(path string) (err error) + Link(oldName string, newName string, linkType AfcLinkType) (err error) + Truncate(filePath string, size int64) (err error) + SetFileModTime(filePath string, modTime time.Time) (err error) + // Hash sha1 algorithm + Hash(filePath string) ([]byte, error) + // HashWithRange sha1 algorithm with file range + HashWithRange(filePath string, start, end uint64) ([]byte, error) + RemoveAll(path string) (err error) + + WriteFile(filename string, data []byte, perm AfcFileMode) (err error) +} + +type HouseArrest interface { + Documents(bundleID string) (afc Afc, err error) + Container(bundleID string) (afc Afc, err error) +} + +type XCTestManagerDaemon interface { + // initiateControlSession iOS 11+ + initiateControlSession(XcodeVersion uint64) (err error) + startExecutingTestPlan(XcodeVersion uint64) (err error) + initiateSession(XcodeVersion uint64, nsUUID *nskeyedarchiver.NSUUID) (err error) + // authorizeTestSession iOS 12+ + authorizeTestSession(pid int) (err error) + // initiateControlSessionForTestProcessID <= iOS 9 + initiateControlSessionForTestProcessID(pid int) (err error) + // initiateControlSessionForTestProcessIDProtocolVersion iOS > 9 && iOS < 12 + initiateControlSessionForTestProcessIDProtocolVersion(pid int, XcodeVersion uint64) (err error) + + registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) + close() +} + +type SyslogRelay interface { + Lines() <-chan string + Stop() +} + +type Pcapd interface { + Packet() <-chan []byte + Stop() +} + +type Perfd interface { + Start() (data <-chan []byte, err error) + Stop() +} + +type DiagnosticsRelay interface { + Reboot() error + Shutdown() error +} + +type CrashReportMover interface { + Move(hostDir string, opts ...CrashReportMoverOption) (err error) + walkDir(dirname string, fn func(path string, info *AfcFileInfo)) (err error) +} + +type SpringBoard interface { + GetIconPNGData(bundleId string) (raw *bytes.Buffer, err error) + GetInterfaceOrientation() (orientation OrientationState, err error) +} + +type InnerConn = libimobiledevice.InnerConn + +type LockdownType = libimobiledevice.LockdownType + +type PairRecord = libimobiledevice.PairRecord + +type CoordinateSystem = libimobiledevice.CoordinateSystem + +const ( + CoordinateSystemWGS84 = libimobiledevice.CoordinateSystemWGS84 + CoordinateSystemBD09 = libimobiledevice.CoordinateSystemBD09 + CoordinateSystemGCJ02 = libimobiledevice.CoordinateSystemGCJ02 +) + +type ApplicationType = libimobiledevice.ApplicationType + +const ( + ApplicationTypeSystem = libimobiledevice.ApplicationTypeSystem + ApplicationTypeUser = libimobiledevice.ApplicationTypeUser + ApplicationTypeInternal = libimobiledevice.ApplicationTypeInternal + ApplicationTypeAny = libimobiledevice.ApplicationTypeAny +) + +type installationProxyOption = libimobiledevice.InstallationProxyOption + +type InstallationProxyOption func(*installationProxyOption) + +func WithApplicationType(appType ApplicationType) InstallationProxyOption { + return func(opt *installationProxyOption) { + opt.ApplicationType = appType + } +} + +func WithReturnAttributes(attrs ...string) InstallationProxyOption { + return func(opt *installationProxyOption) { + if len(opt.ReturnAttributes) == 0 { + opt.ReturnAttributes = attrs + } else { + opt.ReturnAttributes = append(opt.ReturnAttributes, attrs...) + } + opt.ReturnAttributes = _removeDuplicate(opt.ReturnAttributes) + } +} + +func WithBundleIDs(BundleIDs ...string) InstallationProxyOption { + return func(opt *installationProxyOption) { + if len(opt.BundleIDs) == 0 { + opt.BundleIDs = BundleIDs + } else { + opt.BundleIDs = append(opt.BundleIDs, BundleIDs...) + } + opt.BundleIDs = _removeDuplicate(opt.BundleIDs) + } +} + +func WithMetaData(b bool) InstallationProxyOption { + return func(opt *installationProxyOption) { + opt.MetaData = b + } +} + +type appLaunchOption struct { + appPath string + environment map[string]interface{} + arguments []interface{} + options map[string]interface{} +} + +type AppLaunchOption func(option *appLaunchOption) + +func WithAppPath(appPath string) AppLaunchOption { + return func(opt *appLaunchOption) { + opt.appPath = appPath + } +} + +func WithEnvironment(environment map[string]interface{}) AppLaunchOption { + return func(opt *appLaunchOption) { + opt.environment = environment + } +} + +func WithArguments(arguments []interface{}) AppLaunchOption { + return func(opt *appLaunchOption) { + opt.arguments = arguments + } +} + +func WithOptions(options map[string]interface{}) AppLaunchOption { + return func(opt *appLaunchOption) { + for k, v := range options { + opt.options[k] = v + } + } +} + +func WithKillExisting(b bool) AppLaunchOption { + return func(opt *appLaunchOption) { + v := uint64(0) + if b { + v = uint64(1) + } + opt.options["KillExisting"] = v + } +} + +type appListOption struct { + appsMatching map[string]interface{} + updateToken string +} + +type AppListOption func(option *appListOption) + +func WithAppsMatching(appsMatching map[string]interface{}) AppListOption { + return func(opt *appListOption) { + opt.appsMatching = appsMatching + } +} + +func WithUpdateToken(updateToken string) AppListOption { + return func(opt *appListOption) { + opt.updateToken = updateToken + } +} + +type Process struct { + IsApplication bool `json:"isApplication"` + Name string `json:"name"` + Pid int `json:"pid"` + RealAppName string `json:"realAppName"` + StartDate time.Time `json:"startDate"` +} + +type crashReportMoverOption struct { + whenDone func(filename string) + keep bool + extract bool +} + +func defaultCrashReportMoverOption() *crashReportMoverOption { + return &crashReportMoverOption{ + whenDone: func(filename string) {}, + keep: false, + } +} + +type CrashReportMoverOption func(opt *crashReportMoverOption) + +func WithKeepCrashReport(b bool) CrashReportMoverOption { + return func(opt *crashReportMoverOption) { + opt.keep = b + } +} + +func WithExtractRawCrashReport(b bool) CrashReportMoverOption { + return func(opt *crashReportMoverOption) { + opt.extract = b + } +} + +func WithWhenMoveIsDone(whenDone func(filename string)) CrashReportMoverOption { + return func(opt *crashReportMoverOption) { + opt.whenDone = whenDone + } +} + +type xcTestOption struct { + appEnv map[string]interface{} + appArgs []interface{} + appOpt map[string]interface{} +} + +func defaultXCTestOption() *xcTestOption { + return &xcTestOption{ + appEnv: make(map[string]interface{}), + appArgs: make([]interface{}, 0, 2), + appOpt: make(map[string]interface{}), + } +} + +type XCTestOption func(opt *xcTestOption) + +func WithXCTestEnv(env map[string]interface{}) XCTestOption { + return func(opt *xcTestOption) { + opt.appEnv = env + } +} + +// func WithXCTestArgs(args []interface{}) XCTestOption { +// return func(opt *xcTestOption) { +// opt.appArgs = args +// } +// } + +func WithXCTestOpt(appOpt map[string]interface{}) XCTestOption { + return func(opt *xcTestOption) { + opt.appOpt = appOpt + } +} + +func _removeDuplicate(strSlice []string) []string { + existed := make(map[string]bool, len(strSlice)) + noRepeat := make([]string, 0, len(strSlice)) + for _, str := range strSlice { + if _, ok := existed[str]; ok { + continue + } + existed[str] = true + noRepeat = append(noRepeat, str) + } + return noRepeat +} + +func DeviceVersion(version ...int) int { + if len(version) < 3 { + tmp := make([]int, 3) + copy(tmp, version) + version = tmp + } + maj, min, patch := version[0], version[1], version[2] + return ((maj & 0xFF) << 16) | ((min & 0xFF) << 8) | (patch & 0xFF) +} + +var debugFlag = false + +// SetDebug sets debug mode +func SetDebug(debug bool, libDebug ...bool) { + debugFlag = debug + if len(libDebug) >= 1 { + libimobiledevice.SetDebug(libDebug[0]) + } +} + +func debugLog(msg string) { + if !debugFlag { + return + } + fmt.Printf("[go-iDevice-debug] %s\n", msg) +} diff --git a/hrp/pkg/gidevice/imagemounter.go b/hrp/pkg/gidevice/imagemounter.go new file mode 100644 index 00000000..aaa479fb --- /dev/null +++ b/hrp/pkg/gidevice/imagemounter.go @@ -0,0 +1,142 @@ +package gidevice + +import ( + "fmt" + "os" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +var _ ImageMounter = (*imageMounter)(nil) + +func newImageMounter(client *libimobiledevice.ImageMounterClient) *imageMounter { + return &imageMounter{ + client: client, + } +} + +type imageMounter struct { + client *libimobiledevice.ImageMounterClient +} + +func (m *imageMounter) Images(imgType string) (imageSignatures [][]byte, err error) { + var pkt libimobiledevice.Packet + if pkt, err = m.client.NewXmlPacket( + m.client.NewBasicRequest(libimobiledevice.CommandTypeLookupImage, imgType), + ); err != nil { + return nil, err + } + + if err = m.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = m.client.ReceivePacket(); err != nil { + return nil, err + } + + var reply libimobiledevice.ImageMounterLookupImageResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + + imageSignatures = reply.ImageSignature + return +} + +func (m *imageMounter) UploadImage(imgType, dmgPath string, signatureData []byte) (err error) { + var dmgFileInfo os.FileInfo + if dmgFileInfo, err = os.Stat(dmgPath); err != nil { + return err + } + + var pkt libimobiledevice.Packet + if pkt, err = m.client.NewXmlPacket( + m.client.NewReceiveBytesRequest(imgType, uint32(dmgFileInfo.Size()), signatureData), + ); err != nil { + return err + } + + if err = m.client.SendPacket(pkt); err != nil { + return err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = m.client.ReceivePacket(); err != nil { + return err + } + + var reply libimobiledevice.ImageMounterBasicResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + + if reply.Status != "ReceiveBytesAck" { + return fmt.Errorf("image mounter 'ReceiveBytes' status: %s", reply.Status) + } + + var dmgData []byte + if dmgData, err = os.ReadFile(dmgPath); err != nil { + return err + } + + if err = m.client.SendDmg(dmgData); err != nil { + return err + } + if respPkt, err = m.client.ReceivePacket(); err != nil { + return err + } + + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + + if reply.Status != "Complete" { + return fmt.Errorf("image mounter 'SendDmg' status: %s", reply.Status) + } + + return +} + +func (m *imageMounter) Mount(imgType, devImgPath string, signatureData []byte) (err error) { + var pkt libimobiledevice.Packet + if pkt, err = m.client.NewXmlPacket( + m.client.NewMountImageRequest(imgType, devImgPath, signatureData), + ); err != nil { + return err + } + + if err = m.client.SendPacket(pkt); err != nil { + return err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = m.client.ReceivePacket(); err != nil { + return err + } + + var reply libimobiledevice.ImageMounterBasicResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + + if reply.Status != "Complete" { + return fmt.Errorf("image mounter 'MountImage' status: %s", reply.Status) + } + return +} + +func (m *imageMounter) UploadImageAndMount(imgType, devImgPath, dmgPath, signaturePath string) (err error) { + var signatureData []byte + if signatureData, err = os.ReadFile(signaturePath); err != nil { + return err + } + if err = m.UploadImage(imgType, dmgPath, signatureData); err != nil { + return err + } + if err = m.Mount(imgType, devImgPath, signatureData); err != nil { + return err + } + return +} diff --git a/hrp/pkg/gidevice/imagemounter_test.go b/hrp/pkg/gidevice/imagemounter_test.go new file mode 100644 index 00000000..d42f3583 --- /dev/null +++ b/hrp/pkg/gidevice/imagemounter_test.go @@ -0,0 +1,49 @@ +package gidevice + +import ( + "encoding/base64" + "testing" +) + +var imageMounterSrv ImageMounter + +func setupImageMounterSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + // Once + // dev.Images() + if imageMounterSrv, err = lockdownSrv.ImageMounterService(); err != nil { + t.Fatal(err) + } +} + +func Test_imageMounter_Images(t *testing.T) { + setupImageMounterSrv(t) + + // imageSignatures, err := dev.Images() + imageSignatures, err := imageMounterSrv.Images("Developer") + if err != nil { + t.Fatal(err) + } + + for i, imgSign := range imageSignatures { + t.Logf("%2d, %s", i+1, base64.StdEncoding.EncodeToString(imgSign)) + } +} + +func Test_imageMounter_UploadImageAndMount(t *testing.T) { + setupImageMounterSrv(t) + + devImgPath := "/private/var/mobile/Media/PublicStaging/staging.dimage" + dmgPath := "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/14.4/DeveloperDiskImage.dmg" + signaturePath := "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/14.4/DeveloperDiskImage.dmg.signature" + + if err := imageMounterSrv.UploadImageAndMount("Developer", devImgPath, dmgPath, signaturePath); err != nil { + t.Fatal(err) + } +} diff --git a/hrp/pkg/gidevice/installationproxy.go b/hrp/pkg/gidevice/installationproxy.go new file mode 100644 index 00000000..7091880d --- /dev/null +++ b/hrp/pkg/gidevice/installationproxy.go @@ -0,0 +1,167 @@ +package gidevice + +import ( + "fmt" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +var _ InstallationProxy = (*installationProxy)(nil) + +func newInstallationProxy(client *libimobiledevice.InstallationProxyClient) *installationProxy { + return &installationProxy{ + client: client, + } +} + +type installationProxy struct { + client *libimobiledevice.InstallationProxyClient +} + +func (p *installationProxy) Browse(opts ...InstallationProxyOption) (currentList []interface{}, err error) { + opt := new(installationProxyOption) + if len(opts) == 0 { + opt = nil + } else { + for _, optFunc := range opts { + optFunc(opt) + } + } + + var pkt libimobiledevice.Packet + if pkt, err = p.client.NewXmlPacket( + p.client.NewBasicRequest(libimobiledevice.CommandTypeBrowse, opt), + ); err != nil { + return nil, err + } + + if err = p.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = p.client.ReceivePacket(); err != nil { + return nil, err + } + + var reply libimobiledevice.InstallationProxyBrowseResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + + for reply.Status != "Complete" { + if respPkt, err = p.client.ReceivePacket(); err != nil { + return nil, err + } + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + } + + currentList = reply.CurrentList + return +} + +func (p *installationProxy) Lookup(opts ...InstallationProxyOption) (lookupResult interface{}, err error) { + opt := new(installationProxyOption) + if len(opts) == 0 { + opt = nil + } else { + for _, optFunc := range opts { + optFunc(opt) + } + } + + var pkt libimobiledevice.Packet + if pkt, err = p.client.NewXmlPacket( + p.client.NewBasicRequest(libimobiledevice.CommandTypeLookup, opt), + ); err != nil { + return nil, err + } + + if err = p.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = p.client.ReceivePacket(); err != nil { + return nil, err + } + + var reply libimobiledevice.InstallationProxyLookupResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + if reply.Status != "Complete" { + return nil, fmt.Errorf("installation proxy 'Lookup' status: %s", reply.Status) + } + + lookupResult = reply.LookupResult + + return +} + +func (p *installationProxy) Install(bundleID, packagePath string) (err error) { + var pkt libimobiledevice.Packet + if pkt, err = p.client.NewXmlPacket( + p.client.NewInstallRequest(bundleID, packagePath), + ); err != nil { + return err + } + + if err = p.client.SendPacket(pkt); err != nil { + return err + } + + var reply libimobiledevice.InstallationProxyInstallResponse + for len(reply.Error) == 0 { + var respPkt libimobiledevice.Packet + if respPkt, err = p.client.ReceivePacket(); err != nil { + return err + } + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + if reply.Status == "Complete" { + break + } + } + + if len(reply.Error) != 0 { + return fmt.Errorf("installation proxy 'Install' status: %s (err: %s, desc: %s)", reply.Status, reply.Error, reply.ErrorDescription) + } + + return +} + +func (p *installationProxy) Uninstall(bundleID string) (err error) { + var pkt libimobiledevice.Packet + if pkt, err = p.client.NewXmlPacket( + p.client.NewUninstallRequest(bundleID), + ); err != nil { + return err + } + + if err = p.client.SendPacket(pkt); err != nil { + return err + } + + var reply libimobiledevice.InstallationProxyInstallResponse + for len(reply.Error) == 0 { + var respPkt libimobiledevice.Packet + if respPkt, err = p.client.ReceivePacket(); err != nil { + return err + } + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + if reply.Status == "Complete" { + break + } + } + + if len(reply.Error) != 0 { + return fmt.Errorf("installation proxy 'Uninstall' status: %s (err: %s, desc: %s)", reply.Status, reply.Error, reply.ErrorDescription) + } + return +} diff --git a/hrp/pkg/gidevice/installationproxy_test.go b/hrp/pkg/gidevice/installationproxy_test.go new file mode 100644 index 00000000..d4298568 --- /dev/null +++ b/hrp/pkg/gidevice/installationproxy_test.go @@ -0,0 +1,71 @@ +package gidevice + +import ( + "testing" +) + +var installationProxySrv InstallationProxy + +func setupInstallationProxySrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if installationProxySrv, err = lockdownSrv.InstallationProxyService(); err != nil { + t.Fatal(err) + } +} + +func Test_installationProxy_Browse(t *testing.T) { + setupInstallationProxySrv(t) + + // currentList, err := installationProxySrv.Browse(WithMetaData(true)) + // currentList, err := installationProxySrv.Browse(WithReturnAttributes("CFBundleIdentifier", "SequenceNumber", "SequenceNumber")) + // currentList, err := installationProxySrv.Browse(WithApplicationType(ApplicationTypeSystem)) + // currentList, err := installationProxySrv.Browse(WithApplicationType(ApplicationTypeSystem), WithReturnAttributes("ApplicationType", "ApplicationType")) + // currentList, err := dev.InstallationProxyBrowse() + currentList, err := installationProxySrv.Browse() + // currentList, err := installationProxySrv.Browse(WithBundleIDs("com.apple.MusicUIService"), WithBundleIDs("com.apple.Home.HomeControlService")) + if err != nil { + t.Fatal(err) + } + + t.Log(len(currentList)) + + for _, cl := range currentList { + app, ok := cl.(map[string]interface{}) + if ok { + t.Log(app) + } else { + t.Log(cl) + } + } +} + +func Test_installationProxy_Lookup(t *testing.T) { + setupInstallationProxySrv(t) + + // lookupResult, err := installationProxySrv.Lookup() + // lookupResult, err := dev.InstallationProxyLookup( + lookupResult, err := installationProxySrv.Lookup( + // WithApplicationType(ApplicationTypeUser), + // WithApplicationType(ApplicationTypeSystem), + // WithReturnAttributes("CFBundleDevelopmentRegion"), + // WithReturnAttributes("CFBundleDisplayName", "CFBundleIdentifier"), + // WithBundleIDs("com.apple.mobilephone"), + WithBundleIDs("com.leixipaopao.WebDriverAgentRunner.xctrunner"), + ) + if err != nil { + t.Fatal(err) + } + + ret := lookupResult.(map[string]interface{}) + t.Log(len(ret)) + + for k, v := range ret { + t.Log(k, "-->", v) + } +} diff --git a/hrp/pkg/gidevice/instruments.go b/hrp/pkg/gidevice/instruments.go new file mode 100644 index 00000000..b3e74112 --- /dev/null +++ b/hrp/pkg/gidevice/instruments.go @@ -0,0 +1,348 @@ +package gidevice + +import ( + "encoding/json" + "fmt" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +// instruments services +const ( + instrumentsServiceDeviceInfo = "com.apple.instruments.server.services.deviceinfo" + instrumentsServiceProcessControl = "com.apple.instruments.server.services.processcontrol" + instrumentsServiceDeviceApplictionListing = "com.apple.instruments.server.services.device.applictionListing" + instrumentsServiceGraphicsOpengl = "com.apple.instruments.server.services.graphics.opengl" // 获取 GPU/FPS + instrumentsServiceSysmontap = "com.apple.instruments.server.services.sysmontap" // 获取 CPU/Mem/Disk/Network 性能数据 + instrumentsServiceNetworking = "com.apple.instruments.server.services.networking" // 获取所有网络详情数据 + instrumentsServiceMobileNotifications = "com.apple.instruments.server.services.mobilenotifications" // 监控应用状态 +) + +const ( + instrumentsServiceXcodeNetworkStatistics = "com.apple.xcode.debug-gauge-data-providers.NetworkStatistics" // 获取单进程网络数据 + instrumentsServiceXcodeEnergyStatistics = "com.apple.xcode.debug-gauge-data-providers.Energy" // 获取功耗数据 +) + +var _ Instruments = (*instruments)(nil) + +func newInstruments(client *libimobiledevice.InstrumentsClient) *instruments { + return &instruments{ + client: client, + } +} + +type instruments struct { + client *libimobiledevice.InstrumentsClient +} + +func (i *instruments) notifyOfPublishedCapabilities() (err error) { + _, err = i.client.NotifyOfPublishedCapabilities() + return +} + +func (i *instruments) requestChannel(channel string) (id uint32, err error) { + return i.client.RequestChannel(channel) +} + +func (i *instruments) call(channel, selector string, auxiliaries ...interface{}) ( + result *libimobiledevice.DTXMessageResult, err error) { + + chanID, err := i.requestChannel(channel) + if err != nil { + return nil, err + } + + args := libimobiledevice.NewAuxBuffer() + for _, aux := range auxiliaries { + if err = args.AppendObject(aux); err != nil { + return nil, err + } + } + + return i.client.Invoke(selector, args, chanID, true) +} + +func (i *instruments) getPidByBundleID(bundleID string) (pid int, err error) { + apps, err := i.AppList() + if err != nil { + fmt.Printf("get app list error: %v\n", err) + return 0, err + } + + mapper := make(map[string]interface{}) + for _, app := range apps { + mapper[app.ExecutableName] = app.CFBundleIdentifier + } + + processes, err := i.AppRunningProcesses() + if err != nil { + fmt.Printf("get running app processes error: %v\n", err) + return 0, err + } + for _, proc := range processes { + b, ok := mapper[proc.Name] + if ok && bundleID == b { + fmt.Printf("get pid %d by bundleId %s\n", proc.Pid, bundleID) + return proc.Pid, nil + } + } + + fmt.Printf("can't find pid by bundleID: %s\n", bundleID) + return 0, fmt.Errorf("can't find pid by bundleID: %s", bundleID) +} + +func (i *instruments) AppLaunch(bundleID string, opts ...AppLaunchOption) (pid int, err error) { + opt := new(appLaunchOption) + opt.appPath = "" + opt.options = map[string]interface{}{ + "StartSuspendedKey": uint64(0), + "KillExisting": uint64(0), + } + if len(opts) != 0 { + for _, optFunc := range opts { + optFunc(opt) + } + } + + var id uint32 + if id, err = i.requestChannel(instrumentsServiceProcessControl); err != nil { + return 0, err + } + + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(opt.appPath); err != nil { + return 0, err + } + if err = args.AppendObject(bundleID); err != nil { + return 0, err + } + if err = args.AppendObject(opt.environment); err != nil { + return 0, err + } + if err = args.AppendObject(opt.arguments); err != nil { + return 0, err + } + if err = args.AppendObject(opt.options); err != nil { + return 0, err + } + + var result *libimobiledevice.DTXMessageResult + selector := "launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:options:" + if result, err = i.client.Invoke(selector, args, id, true); err != nil { + return 0, err + } + + if nsErr, ok := result.Obj.(libimobiledevice.NSError); ok { + return 0, fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + + return int(result.Obj.(uint64)), nil +} + +func (i *instruments) appProcess(bundleID string) (err error) { + var id uint32 + if id, err = i.requestChannel(instrumentsServiceProcessControl); err != nil { + return err + } + + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(bundleID); err != nil { + return err + } + + selector := "processIdentifierForBundleIdentifier:" + if _, err = i.client.Invoke(selector, args, id, true); err != nil { + return err + } + + return +} + +func (i *instruments) startObserving(pid int) (err error) { + var id uint32 + if id, err = i.requestChannel(instrumentsServiceProcessControl); err != nil { + return err + } + + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(pid); err != nil { + return err + } + + var result *libimobiledevice.DTXMessageResult + selector := "startObservingPid:" + if result, err = i.client.Invoke(selector, args, id, true); err != nil { + return err + } + + if nsErr, ok := result.Obj.(libimobiledevice.NSError); ok { + return fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + return +} + +func (i *instruments) AppKill(pid int) (err error) { + var id uint32 + if id, err = i.requestChannel(instrumentsServiceProcessControl); err != nil { + return err + } + + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(pid); err != nil { + return err + } + + selector := "killPid:" + if _, err = i.client.Invoke(selector, args, id, false); err != nil { + return err + } + + return +} + +func (i *instruments) AppRunningProcesses() (processes []Process, err error) { + var id uint32 + if id, err = i.requestChannel(instrumentsServiceDeviceInfo); err != nil { + return nil, err + } + + selector := "runningProcesses" + + var result *libimobiledevice.DTXMessageResult + if result, err = i.client.Invoke(selector, libimobiledevice.NewAuxBuffer(), id, true); err != nil { + return nil, err + } + + objs := result.Obj.([]interface{}) + + processes = make([]Process, 0, len(objs)) + + for _, v := range objs { + m := v.(map[string]interface{}) + + var data []byte + if data, err = json.Marshal(m); err != nil { + debugLog(fmt.Sprintf("process marshal: %v\n%v\n", err, m)) + err = nil + continue + } + + var tp Process + if err = json.Unmarshal(data, &tp); err != nil { + debugLog(fmt.Sprintf("process unmarshal: %v\n%v\n", err, m)) + err = nil + continue + } + + processes = append(processes, tp) + } + + return +} + +func (i *instruments) AppList(opts ...AppListOption) (apps []Application, err error) { + opt := new(appListOption) + opt.updateToken = "" + opt.appsMatching = make(map[string]interface{}) + if len(opts) != 0 { + for _, optFunc := range opts { + optFunc(opt) + } + } + + var id uint32 + if id, err = i.requestChannel(instrumentsServiceDeviceApplictionListing); err != nil { + return nil, err + } + + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(opt.appsMatching); err != nil { + return nil, err + } + if err = args.AppendObject(opt.updateToken); err != nil { + return nil, err + } + + selector := "installedApplicationsMatching:registerUpdateToken:" + + var result *libimobiledevice.DTXMessageResult + if result, err = i.client.Invoke(selector, args, id, true); err != nil { + return nil, err + } + + objs := result.Obj.([]interface{}) + + for _, v := range objs { + m := v.(map[string]interface{}) + + var data []byte + if data, err = json.Marshal(m); err != nil { + debugLog(fmt.Sprintf("application marshal: %v\n%v\n", err, m)) + err = nil + continue + } + + var app Application + if err = json.Unmarshal(data, &app); err != nil { + debugLog(fmt.Sprintf("application unmarshal: %v\n%v\n", err, m)) + err = nil + continue + } + apps = append(apps, app) + } + + return +} + +func (i *instruments) DeviceInfo() (devInfo *DeviceInfo, err error) { + var id uint32 + if id, err = i.requestChannel(instrumentsServiceDeviceInfo); err != nil { + return nil, err + } + + selector := "systemInformation" + + var result *libimobiledevice.DTXMessageResult + if result, err = i.client.Invoke(selector, libimobiledevice.NewAuxBuffer(), id, true); err != nil { + return nil, err + } + + data, err := json.Marshal(result.Obj) + if err != nil { + return nil, err + } + devInfo = new(DeviceInfo) + err = json.Unmarshal(data, devInfo) + + return +} + +func (i *instruments) registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) { + i.client.RegisterCallback(obj, cb) +} + +type Application struct { + AppExtensionUUIDs []string `json:"AppExtensionUUIDs,omitempty"` + BundlePath string `json:"BundlePath"` + CFBundleIdentifier string `json:"CFBundleIdentifier"` + ContainerBundleIdentifier string `json:"ContainerBundleIdentifier,omitempty"` + ContainerBundlePath string `json:"ContainerBundlePath,omitempty"` + DisplayName string `json:"DisplayName"` + ExecutableName string `json:"ExecutableName,omitempty"` + Placeholder bool `json:"Placeholder,omitempty"` + PluginIdentifier string `json:"PluginIdentifier,omitempty"` + PluginUUID string `json:"PluginUUID,omitempty"` + Restricted int `json:"Restricted"` + Type string `json:"Type"` + Version string `json:"Version"` +} + +type DeviceInfo struct { + Description string `json:"_deviceDescription"` + DisplayName string `json:"_deviceDisplayName"` + Identifier string `json:"_deviceIdentifier"` + Version string `json:"_deviceVersion"` + ProductType string `json:"_productType"` + ProductVersion string `json:"_productVersion"` + XRDeviceClassName string `json:"_xrdeviceClassName"` +} diff --git a/hrp/pkg/gidevice/instruments_test.go b/hrp/pkg/gidevice/instruments_test.go new file mode 100644 index 00000000..5da93259 --- /dev/null +++ b/hrp/pkg/gidevice/instruments_test.go @@ -0,0 +1,97 @@ +package gidevice + +import ( + "testing" +) + +var ( + instrumentsSrv Instruments + bundleID = "com.apple.Preferences" +) + +func setupInstrumentsSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if instrumentsSrv, err = lockdownSrv.InstrumentsService(); err != nil { + t.Fatal(err) + } +} + +func Test_instruments_AppLaunch(t *testing.T) { + setupInstrumentsSrv(t) + + // bundleID = "com.leixipaopao.WebDriverAgentRunner.xctrunner" + + // pid, err := dev.AppLaunch(bundleID) + pid, err := instrumentsSrv.AppLaunch(bundleID) + // pid, err := instrumentsSrv.AppLaunch(bundleID, WithKillExisting(true)) + // pid, err := instrumentsSrv.AppLaunch(bundleID, WithKillExisting(true), WithArguments([]interface{}{"-AppleLanguages", "(Russian)"})) + if err != nil { + t.Fatal(err) + } + t.Log(pid) +} + +func Test_instruments_AppKill(t *testing.T) { + setupInstrumentsSrv(t) + + pid, err := instrumentsSrv.AppLaunch(bundleID) + if err != nil { + t.Fatal(err) + } + t.Log(pid) + + // if err = dev.AppKill(pid); err != nil { + if err = instrumentsSrv.AppKill(pid); err != nil { + t.Fatal(err) + } +} + +func Test_instruments_AppRunningProcesses(t *testing.T) { + setupInstrumentsSrv(t) + + // processes, err := dev.AppRunningProcesses() + processes, err := instrumentsSrv.AppRunningProcesses() + if err != nil { + t.Fatal(err) + } + + for _, p := range processes { + t.Log(p.IsApplication, "\t", p.Pid, "\t", p.Name, "\t", p.RealAppName, "\t", p.StartDate) + } +} + +func Test_instruments_AppList(t *testing.T) { + setupInstrumentsSrv(t) + + // apps, err := dev.AppList() + apps, err := instrumentsSrv.AppList() + if err != nil { + t.Fatal(err) + } + + for _, app := range apps { + t.Logf("%v\t%v\t%v\t%v\t%v\n", app.Type, app.DisplayName, app.ExecutableName, app.AppExtensionUUIDs, app.BundlePath) + } +} + +func Test_instruments_DeviceInfo(t *testing.T) { + setupInstrumentsSrv(t) + + devInfo, err := instrumentsSrv.DeviceInfo() + if err != nil { + t.Fatal(err) + } + t.Log(devInfo.Description) + t.Log(devInfo.DisplayName) + t.Log(devInfo.Identifier) + t.Log(devInfo.Version) + t.Log(devInfo.ProductType) + t.Log(devInfo.ProductVersion) + t.Log(devInfo.XRDeviceClassName) +} diff --git a/hrp/pkg/gidevice/lockdown.go b/hrp/pkg/gidevice/lockdown.go new file mode 100644 index 00000000..2f2d80d7 --- /dev/null +++ b/hrp/pkg/gidevice/lockdown.go @@ -0,0 +1,684 @@ +package gidevice + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "math/big" + "strconv" + "strings" + "time" + + uuid "github.com/satori/go.uuid" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +var _ Lockdown = (*lockdown)(nil) + +func newLockdown(dev *device) *lockdown { + return &lockdown{ + umClient: dev.umClient, + client: dev.lockdownClient, + dev: dev, + } +} + +type lockdown struct { + umClient *libimobiledevice.UsbmuxClient + client *libimobiledevice.LockdownClient + sessionID string + + dev *device + iOSVersion []int + pairRecord *PairRecord +} + +func (c *lockdown) QueryType() (LockdownType, error) { + pkt, err := c.client.NewXmlPacket( + c.client.NewBasicRequest(libimobiledevice.RequestTypeQueryType), + ) + if err != nil { + return LockdownType{}, err + } + + if err = c.client.SendPacket(pkt); err != nil { + return LockdownType{}, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = c.client.ReceivePacket(); err != nil { + return LockdownType{}, err + } + + var reply libimobiledevice.LockdownTypeResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return LockdownType{}, err + } + + return LockdownType{Type: reply.Type}, nil +} + +func (c *lockdown) GetValue(domain, key string) (v interface{}, err error) { + pkt, err := c.client.NewXmlPacket( + c.client.NewGetValueRequest(domain, key), + ) + if err != nil { + return nil, err + } + + if err = c.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = c.client.ReceivePacket(); err != nil { + return nil, err + } + + var reply libimobiledevice.LockdownValueResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + + v = reply.Value + + return +} + +func (c *lockdown) SetValue(domain, key string, value interface{}) (err error) { + var pkt libimobiledevice.Packet + if pkt, err = c.client.NewXmlPacket( + c.client.NewSetValueRequest(domain, key, value), + ); err != nil { + return err + } + + if err = c.client.SendPacket(pkt); err != nil { + return err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = c.client.ReceivePacket(); err != nil { + return err + } + + var reply libimobiledevice.LockdownValueResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + + if !reply.Value.(bool) { + return errors.New("lockdown SetValue: Failed") + } + return +} + +func (c *lockdown) EnterRecovery() (err error) { + var pkt libimobiledevice.Packet + if pkt, err = c.client.NewXmlPacket( + c.client.NewEnterRecoveryRequest(), + ); err != nil { + return err + } + + if err = c.client.SendPacket(pkt); err != nil { + return err + } + + if _, err = c.client.ReceivePacket(); err != nil { + return err + } + + return +} + +func (c *lockdown) handshake() (err error) { + var lockdownType LockdownType + if lockdownType, err = c.QueryType(); err != nil { + return err + } + if lockdownType.Type != "com.apple.mobile.lockdown" { + return fmt.Errorf("lockdown handshake 'QueryType': %s", lockdownType.Type) + } + + // if (device->version < DEVICE_VERSION(7,0,0)) + // for older devices, we need to validate pairing to receive trusted host status + + if c.pairRecord, err = c.dev.ReadPairRecord(); err == nil { + return nil + } + + if !strings.Contains(err.Error(), libimobiledevice.ReplyCodeBadDevice.String()) { + return err + } + + if c.pairRecord, err = c.Pair(); err != nil { + return err + } + + err = c.dev.SavePairRecord(c.pairRecord) + + return +} + +func (c *lockdown) Pair() (pairRecord *PairRecord, err error) { + var buid string + if buid, err = newUsbmux(c.umClient).ReadBUID(); err != nil { + return nil, err + } + + var devPublicKeyPem []byte + var devWiFiAddr string + + if lockdownValue, err := c.GetValue("", "DevicePublicKey"); err != nil { + return nil, err + } else { + devPublicKeyPem = lockdownValue.([]byte) + } + if lockdownValue, err := c.GetValue("", "WiFiAddress"); err != nil { + return nil, err + } else { + devWiFiAddr = lockdownValue.(string) + } + + if pairRecord, err = generatePairRecord(devPublicKeyPem); err != nil { + return nil, err + } + + pairRecord.SystemBUID = buid + pairRecord.HostID = strings.ToUpper(uuid.NewV4().String()) + hostPrivateKey := pairRecord.HostPrivateKey + pairRecord.HostPrivateKey = nil + rootPrivateKey := pairRecord.RootPrivateKey + pairRecord.RootPrivateKey = nil + + var pkt libimobiledevice.Packet + if pkt, err = c.client.NewXmlPacket( + c.client.NewPairRequest(pairRecord), + ); err != nil { + return nil, err + } + + if err = c.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = c.client.ReceivePacket(); err != nil { + return nil, err + } + + var reply libimobiledevice.LockdownPairResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + + pairRecord.EscrowBag = reply.EscrowBag + pairRecord.WiFiMACAddress = devWiFiAddr + pairRecord.HostPrivateKey = hostPrivateKey + pairRecord.RootPrivateKey = rootPrivateKey + + return +} + +func (c *lockdown) startSession(pairRecord *PairRecord) (err error) { + // if we have a running session, stop current one first + if c.sessionID != "" { + if err = c.stopSession(); err != nil { + return err + } + } + + var pkt libimobiledevice.Packet + if pkt, err = c.client.NewXmlPacket( + c.client.NewStartSessionRequest(pairRecord.SystemBUID, pairRecord.HostID), + ); err != nil { + return err + } + + if err = c.client.SendPacket(pkt); err != nil { + return err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = c.client.ReceivePacket(); err != nil { + return fmt.Errorf("lockdown start session: %w", err) + } + + var reply libimobiledevice.LockdownStartSessionResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + + if reply.EnableSessionSSL { + if err = c.client.EnableSSL(c.iOSVersion, pairRecord); err != nil { + return err + } + } + + c.sessionID = reply.SessionID + return +} + +func (c *lockdown) stopSession() (err error) { + if c.sessionID == "" { + return nil + } + + var pkt libimobiledevice.Packet + if pkt, err = c.client.NewXmlPacket( + c.client.NewStopSessionRequest(c.sessionID), + ); err != nil { + return err + } + + if err = c.client.SendPacket(pkt); err != nil { + return err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = c.client.ReceivePacket(); err != nil { + return fmt.Errorf("lockdown stop session: %w", err) + } + + var reply libimobiledevice.LockdownBasicResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + + c.sessionID = "" + return +} + +func (c *lockdown) startService(service string, escrowBag []byte) (dynamicPort int, enableSSL bool, err error) { + req := c.client.NewStartServiceRequest(service) + if escrowBag != nil { + req.EscrowBag = escrowBag + } + + var pkt libimobiledevice.Packet + if pkt, err = c.client.NewXmlPacket(req); err != nil { + return 0, false, err + } + + if err = c.client.SendPacket(pkt); err != nil { + return 0, false, err + } + + respPkt, err := c.client.ReceivePacket() + if err != nil { + return 0, false, err + } + + var reply libimobiledevice.LockdownStartServiceResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return 0, false, err + } + + if reply.Error != "" { + return 0, false, fmt.Errorf("lockdown start service: %s", reply.Error) + } + + dynamicPort = reply.Port + enableSSL = reply.EnableServiceSSL + + return +} + +func (c *lockdown) ImageMounterService() (imageMounter ImageMounter, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.ImageMounterServiceName, nil); err != nil { + return nil, err + } + imageMounterClient := libimobiledevice.NewImageMounterClient(innerConn) + imageMounter = newImageMounter(imageMounterClient) + return +} + +func (c *lockdown) ScreenshotService() (screenshot Screenshot, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.ScreenshotServiceName, nil); err != nil { + return nil, err + } + screenshotClient := libimobiledevice.NewScreenshotClient(innerConn) + screenshot = newScreenshot(screenshotClient) + return +} + +func (c *lockdown) SimulateLocationService() (simulateLocation SimulateLocation, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.SimulateLocationServiceName, nil); err != nil { + return nil, err + } + simulateLocationClient := libimobiledevice.NewSimulateLocationClient(innerConn) + simulateLocation = newSimulateLocation(simulateLocationClient) + return +} + +func (c *lockdown) InstallationProxyService() (installationProxy InstallationProxy, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.InstallationProxyServiceName, nil); err != nil { + return nil, err + } + installationProxyClient := libimobiledevice.NewInstallationProxyClient(innerConn) + installationProxy = newInstallationProxy(installationProxyClient) + return +} + +func (c *lockdown) InstrumentsService() (instruments Instruments, err error) { + service := libimobiledevice.InstrumentsServiceName + if DeviceVersion(c.iOSVersion...) >= DeviceVersion(14, 0, 0) { + service = libimobiledevice.InstrumentsSecureProxyServiceName + } + + var innerConn InnerConn + if innerConn, err = c._startService(service, nil); err != nil { + return nil, err + } + instrumentsClient := libimobiledevice.NewInstrumentsClient(innerConn) + instruments = newInstruments(instrumentsClient) + + if service == libimobiledevice.InstrumentsServiceName { + _ = innerConn.DismissSSL() + } + + if err = instruments.notifyOfPublishedCapabilities(); err != nil { + return nil, err + } + + return +} + +func (c *lockdown) TestmanagerdService() (testmanagerd Testmanagerd, err error) { + service := libimobiledevice.TestmanagerdServiceName + if DeviceVersion(c.iOSVersion...) >= DeviceVersion(14, 0, 0) { + service = libimobiledevice.TestmanagerdSecureServiceName + } + + var innerConn InnerConn + if innerConn, err = c._startService(service, nil); err != nil { + return nil, err + } + testmanagerdClient := libimobiledevice.NewTestmanagerdClient(innerConn) + testmanagerd = newTestmanagerd(testmanagerdClient, c.iOSVersion) + + if service == libimobiledevice.TestmanagerdServiceName { + _ = innerConn.DismissSSL() + } + + if err = testmanagerd.notifyOfPublishedCapabilities(); err != nil { + return nil, err + } + + return +} + +func (c *lockdown) AfcService() (afc Afc, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.AfcServiceName, nil); err != nil { + return nil, err + } + afcClient := libimobiledevice.NewAfcClient(innerConn) + afc = newAfc(afcClient) + return +} + +func (c *lockdown) HouseArrestService() (houseArrest HouseArrest, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.HouseArrestServiceName, nil); err != nil { + return nil, err + } + houseArrestClient := libimobiledevice.NewHouseArrestClient(innerConn) + houseArrest = newHouseArrest(houseArrestClient) + return +} + +func (c *lockdown) SyslogRelayService() (syslogRelay SyslogRelay, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.SyslogRelayServiceName, nil); err != nil { + return nil, err + } + syslogRelayClient := libimobiledevice.NewSyslogRelayClient(innerConn) + syslogRelay = newSyslogRelay(syslogRelayClient) + return +} + +func (c *lockdown) PcapdService() (pcapd Pcapd, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.PcapdServiceName, nil); err != nil { + return nil, err + } + pcapdClient := libimobiledevice.NewPcapdClient(innerConn) + return newPcapdClient(pcapdClient), nil +} + +func (c *lockdown) DiagnosticsRelayService() (diagnostics DiagnosticsRelay, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.DiagnosticsRelayServiceName, nil); err != nil { + return nil, err + } + diagnosticsRelayClient := libimobiledevice.NewDiagnosticsRelayClient(innerConn) + diagnostics = newDiagnosticsRelay(diagnosticsRelayClient) + + return +} + +func (c *lockdown) SpringBoardService() (springboard SpringBoard, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.SpringBoardServiceName, nil); err != nil { + return nil, err + } + springBoardServiceClient := libimobiledevice.NewSpringBoardClient(innerConn) + springboard = newSpringBoard(springBoardServiceClient) + return +} + +func (c *lockdown) CrashReportMoverService() (crashReportMover CrashReportMover, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.CrashReportMoverServiceName, nil); err != nil { + return nil, err + } + + mover := newCrashReportMover(libimobiledevice.NewCrashReportMoverClient(innerConn)) + if err = mover.readPing(); err != nil { + return nil, err + } + + if innerConn, err = c._startService(libimobiledevice.CrashReportCopyMobileServiceName, nil); err != nil { + return nil, err + } + mover.afc = newAfc(libimobiledevice.NewAfcClient(innerConn)) + + crashReportMover = mover + return +} + +func (c *lockdown) _startService(serviceName string, escrowBag []byte) (innerConn InnerConn, err error) { + if err = c.handshake(); err != nil { + return nil, err + } + + if err = c.startSession(c.pairRecord); err != nil { + return nil, err + } + + dynamicPort, enableSSL, err := c.startService(serviceName, escrowBag) + if err != nil { + return nil, err + } + + if err = c.stopSession(); err != nil { + return nil, err + } + + if innerConn, err = c.dev.NewConnect(dynamicPort, 0); err != nil { + return nil, err + } + // clean deadline + innerConn.Timeout(0) + + if enableSSL { + if err = innerConn.Handshake(c.iOSVersion, c.pairRecord); err != nil { + return nil, err + } + } + return +} + +func (c *lockdown) _getProductVersion() (version []int, err error) { + if c.iOSVersion != nil { + return c.iOSVersion, nil + } + + var devProductVersion []string + if lockdownValue, err := c.GetValue("", "ProductVersion"); err != nil { + return nil, err + } else { + devProductVersion = strings.Split(lockdownValue.(string), ".") + } + + version = make([]int, len(devProductVersion)) + for i, v := range devProductVersion { + version[i], _ = strconv.Atoi(v) + } + + // if len(version) == 2 { + // version = append(version, 0) + // } + c.iOSVersion = version + + return +} + +func generatePairRecord(devPublicKeyPem []byte) (pairRecord *PairRecord, err error) { + block, _ := pem.Decode(devPublicKeyPem) + var deviceKey *rsa.PublicKey + if deviceKey, err = x509.ParsePKCS1PublicKey(block.Bytes); err != nil { + return nil, err + } + + var rootKey, hostKey *rsa.PrivateKey + if rootKey, err = rsa.GenerateKey(rand.Reader, 2048); err != nil { + return nil, err + } + if hostKey, err = rsa.GenerateKey(rand.Reader, 2048); err != nil { + return nil, err + } + serialNumber := big.NewInt(0) + notBefore := time.Now() + notAfter := notBefore.Add(time.Hour * (24 * 365) * 10) + + rootTemplate := x509.Certificate{ + IsCA: true, + SerialNumber: serialNumber, + Version: 2, + SignatureAlgorithm: x509.SHA1WithRSA, + PublicKeyAlgorithm: x509.RSA, + NotBefore: notBefore, + NotAfter: notAfter, + BasicConstraintsValid: true, + } + + var caCert, cert []byte + if caCert, err = x509.CreateCertificate(rand.Reader, &rootTemplate, &rootTemplate, rootKey.Public(), rootKey); err != nil { + return nil, err + } + + hostTemplate := x509.Certificate{ + SerialNumber: serialNumber, + Version: 2, + SignatureAlgorithm: x509.SHA1WithRSA, + PublicKeyAlgorithm: x509.RSA, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + } + if cert, err = x509.CreateCertificate(rand.Reader, &hostTemplate, &rootTemplate, hostKey.Public(), rootKey); err != nil { + return nil, err + } + + h := sha1.New() + if _, err = h.Write(rootKey.N.Bytes()); err != nil { + return nil, err + } + subjectKeyId := h.Sum(nil) + + deviceTemplate := x509.Certificate{ + SerialNumber: serialNumber, + Version: 2, + SignatureAlgorithm: x509.SHA1WithRSA, + PublicKeyAlgorithm: x509.RSA, + NotBefore: notBefore, + NotAfter: notAfter, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + SubjectKeyId: subjectKeyId, + } + + var deviceCert []byte + if deviceCert, err = x509.CreateCertificate(rand.Reader, &deviceTemplate, &rootTemplate, deviceKey, rootKey); err != nil { + return nil, err + } + + var deviceCertPEM []byte + if deviceCertPEM, err = encodePemCertificate(deviceCert); err != nil { + return nil, err + } + + var caPEM, caPrivatePEM []byte + if caPEM, caPrivatePEM, err = encodePairPemFormat(caCert, rootKey); err != nil { + return nil, err + } + + var certPEM, certPrivatePEM []byte + if certPEM, certPrivatePEM, err = encodePairPemFormat(cert, hostKey); err != nil { + return nil, err + } + + pairRecord = new(PairRecord) + + pairRecord.DeviceCertificate = deviceCertPEM + pairRecord.HostCertificate = certPEM + pairRecord.HostPrivateKey = certPrivatePEM + pairRecord.RootCertificate = caPEM + pairRecord.RootPrivateKey = caPrivatePEM + + return +} + +func encodePairPemFormat(cert []byte, key *rsa.PrivateKey) ([]byte, []byte, error) { + p, err := encodePemCertificate(cert) + if err != nil { + return nil, nil, err + } + + buf := new(bytes.Buffer) + if err := pem.Encode(buf, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }); err != nil { + return nil, nil, err + } + + privy := buf.Bytes() + + return p, privy, nil +} + +func encodePemCertificate(cert []byte) ([]byte, error) { + buf := new(bytes.Buffer) + if err := pem.Encode(buf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert, + }); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/hrp/pkg/gidevice/lockdown_test.go b/hrp/pkg/gidevice/lockdown_test.go new file mode 100644 index 00000000..da4e975f --- /dev/null +++ b/hrp/pkg/gidevice/lockdown_test.go @@ -0,0 +1,106 @@ +package gidevice + +import ( + "fmt" + "os" + "os/signal" + "path" + "testing" + "time" +) + +var lockdownSrv Lockdown + +func setupLockdownSrv(t *testing.T) { + setupDevice(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } +} + +func Test_lockdown_QueryType(t *testing.T) { + setupLockdownSrv(t) + + lockdownType, err := lockdownSrv.QueryType() + if err != nil { + t.Fatal(err) + } + + t.Log(lockdownType.Type) +} + +func Test_lockdown_GetValue(t *testing.T) { + setupLockdownSrv(t) + + // v, err := dev.GetValue("com.apple.mobile.iTunes", "") + // v, err := dev.GetValue("com.apple.mobile.internal", "") + v, err := dev.GetValue("com.apple.mobile.battery", "") + // v, err := lockdownSrv.GetValue("", "ProductVersion") + // v, err := lockdownSrv.GetValue("", "DeviceName") + // v, err := lockdownSrv.GetValue("com.apple.mobile.iTunes", "") + // v, err := lockdownSrv.GetValue("com.apple.mobile.battery", "") + // v, err := lockdownSrv.GetValue("com.apple.disk_usage", "") + if err != nil { + t.Fatal(err) + } + + t.Log(v) +} + +func Test_lockdown_SyslogRelayService(t *testing.T) { + setupLockdownSrv(t) + + syslogRelaySrv, err := lockdownSrv.SyslogRelayService() + if err != nil { + t.Fatal(err) + } + syslogRelaySrv.Stop() + + lines := syslogRelaySrv.Lines() + + done := make(chan os.Signal, 1) + + go func() { + for line := range lines { + fmt.Println(line) + } + done <- os.Interrupt + fmt.Println("DONE!!!") + }() + + signal.Notify(done, os.Interrupt, os.Kill) + + <-done + syslogRelaySrv.Stop() + time.Sleep(time.Second) +} + +func Test_lockdown_CrashReportMoverService(t *testing.T) { + setupLockdownSrv(t) + + crashReportMoverSrv, err := lockdownSrv.CrashReportMoverService() + if err != nil { + t.Fatal(err) + } + + filenames := make([]string, 0, 36) + fn := func(cwd string, info *AfcFileInfo) { + if cwd == "." { + cwd = "" + } + filenames = append(filenames, path.Join(cwd, info.Name())) + // fmt.Println(path.Join(cwd, name)) + } + err = crashReportMoverSrv.walkDir(".", fn) + if err != nil { + t.Fatal(err) + } + + for _, n := range filenames { + fmt.Println(n) + } + + t.Log(len(filenames)) +} diff --git a/hrp/pkg/gidevice/pcapd.go b/hrp/pkg/gidevice/pcapd.go new file mode 100644 index 00000000..6c1c7fcd --- /dev/null +++ b/hrp/pkg/gidevice/pcapd.go @@ -0,0 +1,60 @@ +package gidevice + +import ( + "log" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +type pcapdClient struct { + stop chan struct{} + c *libimobiledevice.PcapdClient +} + +func newPcapdClient(c *libimobiledevice.PcapdClient) *pcapdClient { + return &pcapdClient{ + stop: make(chan struct{}), + c: c, + } +} + +func (c *pcapdClient) Packet() <-chan []byte { + packetCh := make(chan []byte, 10) + go func() { + for { + select { + case <-c.stop: + return + default: + pkt, err := c.c.ReceivePacket() + if err != nil { + close(packetCh) + return + } + var payload []byte + _ = pkt.Unmarshal(&payload) + raw, err := c.c.GetPacket(payload) + if err != nil { + close(packetCh) + return + } + res, err := c.c.CreatePacket(raw) + if err != nil { + log.Println("failed to create packet") + return + } + packetCh <- res + } + } + }() + return packetCh +} + +func (c *pcapdClient) Stop() { + select { + case <-c.stop: + default: + close(c.stop) + } + c.c.Close() +} diff --git a/hrp/pkg/gidevice/perfd.go b/hrp/pkg/gidevice/perfd.go new file mode 100644 index 00000000..334e879e --- /dev/null +++ b/hrp/pkg/gidevice/perfd.go @@ -0,0 +1,889 @@ +package gidevice + +import ( + "context" + "encoding/binary" + "encoding/json" + "fmt" + "net" + "strconv" + "time" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +type PerfOptions struct { + // system + SysCPU bool `json:"sys_cpu,omitempty" yaml:"sys_cpu,omitempty"` + SysMem bool `json:"sys_mem,omitempty" yaml:"sys_mem,omitempty"` + SysDisk bool `json:"sys_disk,omitempty" yaml:"sys_disk,omitempty"` + SysNetwork bool `json:"sys_network,omitempty" yaml:"sys_network,omitempty"` + gpu bool + FPS bool `json:"fps,omitempty" yaml:"fps,omitempty"` + Network bool `json:"network,omitempty" yaml:"network,omitempty"` + // process + BundleID string `json:"bundle_id,omitempty" yaml:"bundle_id,omitempty"` + Pid int `json:"pid,omitempty" yaml:"pid,omitempty"` + // config + OutputInterval int `json:"output_interval,omitempty" yaml:"output_interval,omitempty"` // ms + SystemAttributes []string `json:"system_attributes,omitempty" yaml:"system_attributes,omitempty"` + ProcessAttributes []string `json:"process_attributes,omitempty" yaml:"process_attributes,omitempty"` +} + +func defaulPerfOption() *PerfOptions { + return &PerfOptions{ + SysCPU: true, // default on + SysMem: true, // default on + SysDisk: false, + SysNetwork: false, + gpu: false, + FPS: false, + Network: false, + OutputInterval: 1000, // default 1000ms + SystemAttributes: []string{ + // disk + "diskBytesRead", + "diskBytesWritten", + "diskReadOps", + "diskWriteOps", + // memory + "vmCompressorPageCount", + "vmExtPageCount", + "vmFreeCount", + "vmIntPageCount", + "vmPurgeableCount", + "vmWireCount", + "vmUsedCount", + "__vmSwapUsage", + // network + "netBytesIn", + "netBytesOut", + "netPacketsIn", + "netPacketsOut", + }, + ProcessAttributes: []string{ + "pid", + "cpuUsage", + }, + } +} + +type PerfOption func(*PerfOptions) + +func WithPerfSystemCPU(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.SysCPU = b + } +} + +func WithPerfSystemMem(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.SysMem = b + } +} + +func WithPerfSystemDisk(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.SysDisk = b + } +} + +func WithPerfSystemNetwork(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.SysNetwork = b + } +} + +func WithPerfBundleID(bundleID string) PerfOption { + return func(opt *PerfOptions) { + opt.BundleID = bundleID + } +} + +func WithPerfPID(pid int) PerfOption { + return func(opt *PerfOptions) { + opt.Pid = pid + } +} + +func WithPerfGPU(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.gpu = b + } +} + +func WithPerfFPS(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.FPS = b + } +} + +func WithPerfNetwork(b bool) PerfOption { + return func(opt *PerfOptions) { + opt.Network = b + } +} + +func WithPerfOutputInterval(intervalMilliseconds int) PerfOption { + return func(opt *PerfOptions) { + opt.OutputInterval = intervalMilliseconds + } +} + +func WithPerfProcessAttributes(attrs ...string) PerfOption { + return func(opt *PerfOptions) { + opt.ProcessAttributes = attrs + } +} + +func WithPerfSystemAttributes(attrs ...string) PerfOption { + return func(opt *PerfOptions) { + opt.SystemAttributes = attrs + } +} + +type perfdClient struct { + options *PerfOptions + i Instruments + stop chan struct{} // used to stop perf client + cancel context.CancelFunc // used to cancel all iterators +} + +func (d *device) newPerfdSysmontap(options *PerfOptions) (*perfdSysmontap, error) { + instruments, err := d.newInstrumentsService() + if err != nil { + return nil, err + } + return &perfdSysmontap{ + perfdClient: perfdClient{ + i: instruments, + options: options, + stop: make(chan struct{}), + }, + chanSysCPU: make(chan []byte, 10), + chanSysMem: make(chan []byte, 10), + chanSysDisk: make(chan []byte, 10), + chanSysNetwork: make(chan []byte, 10), + chanProcess: make(chan []byte, 10), + }, nil +} + +type perfdSysmontap struct { + perfdClient + chanSysCPU chan []byte // system cpu channel + chanSysMem chan []byte // system mem channel + chanSysDisk chan []byte // system disk channel + chanSysNetwork chan []byte // system network channel + chanProcess chan []byte // process channel +} + +func (c *perfdSysmontap) Start() (data <-chan []byte, err error) { + // set config + config := map[string]interface{}{ + "bm": 0, + "cpuUsage": true, + "sampleInterval": time.Second * 1, // 1s + "ur": c.options.OutputInterval, // 输出频率 + "procAttrs": c.options.ProcessAttributes, // process performance + "sysAttrs": c.options.SystemAttributes, // system performance + } + if _, err = c.i.call( + instrumentsServiceSysmontap, + "setConfig:", + config, + ); err != nil { + return nil, err + } + + // start + if _, err = c.i.call( + instrumentsServiceSysmontap, + "start", + ); err != nil { + return nil, err + } + + // register listener + ctx, cancel := context.WithCancel(context.TODO()) + c.i.registerCallback("", func(m libimobiledevice.DTXMessageResult) { + select { + case <-ctx.Done(): + c.i.call(instrumentsServiceSysmontap, "stop") + return + default: + dataArray, ok := m.Obj.([]interface{}) + if !ok || len(dataArray) != 2 { + return + } + + if c.options.Pid != 0 { + c.parseProcessData(dataArray) + } else { + c.parseSystemData(dataArray) + } + } + }) + c.cancel = cancel + + outCh := make(chan []byte, 100) + + go func() { + for { + select { + case <-c.stop: + c.cancel() + return + case cpuBytes, ok := <-c.chanSysCPU: + if ok { + outCh <- cpuBytes + } + case memBytes, ok := <-c.chanSysMem: + if ok { + outCh <- memBytes + } + case diskBytes, ok := <-c.chanSysDisk: + if ok { + outCh <- diskBytes + } + case networkBytes, ok := <-c.chanSysNetwork: + if ok { + outCh <- networkBytes + } + case processBytes, ok := <-c.chanProcess: + if ok { + outCh <- processBytes + } + } + } + }() + + return outCh, nil +} + +func (c *perfdSysmontap) Stop() { + close(c.stop) +} + +func (c *perfdSysmontap) parseProcessData(dataArray []interface{}) { + // dataArray example: + // [ + // map[ + // CPUCount:2 + // EnabledCPUs:2 + // PerCPUUsage:[ + // map[CPU_NiceLoad:0 CPU_SystemLoad:-1 CPU_TotalLoad:3.6363636363636402 CPU_UserLoad:-1] + // map[CPU_NiceLoad:0 CPU_SystemLoad:-1 CPU_TotalLoad:2.7272727272727195 CPU_UserLoad:-1] + // ] + // System:[36408520704 6897049600 3031160 773697 15596 61940 1297 26942 588 17020 127346 1835008 119718056 107009899 174046 103548] + // SystemCPUUsage:map[CPU_NiceLoad:0 CPU_SystemLoad:-1 CPU_TotalLoad:6.36363636363636 CPU_UserLoad:-1] + // StartMachAbsTime:5896602132889 + // EndMachAbsTime:5896628486761 + // Type:41 + // ] + // map[ + // Processes:map[ + // 0:[1.3582834340402803 0] + // 124:[0.011456702068519481 124] + // 136:[0.05468332721703649 136] + // ] + // StartMachAbsTime:5896602295095 + // EndMachAbsTime:5896628780514 + // Type:5 + // ] + // ] + + processData := make(map[string]interface{}) + processData["type"] = "process" + processData["timestamp"] = time.Now().Unix() + processData["pid"] = c.options.Pid + + defer func() { + processBytes, _ := json.Marshal(processData) + c.chanProcess <- processBytes + }() + + systemInfo := dataArray[0].(map[string]interface{}) + processInfo := dataArray[1].(map[string]interface{}) + if _, ok := systemInfo["System"]; !ok { + systemInfo, processInfo = processInfo, systemInfo + } + + var targetProcessValue []interface{} + processList := processInfo["Processes"].(map[string]interface{}) + for pid, v := range processList { + if pid != strconv.Itoa(c.options.Pid) { + continue + } + targetProcessValue = v.([]interface{}) + } + + if targetProcessValue == nil { + processData["msg"] = fmt.Sprintf("process %d not found", c.options.Pid) + return + } + + processAttributesMap := make(map[string]interface{}) + for idx, value := range c.options.ProcessAttributes { + processAttributesMap[value] = targetProcessValue[idx] + } + processData["proc_perf"] = processAttributesMap + + systemAttributesValue := systemInfo["System"].([]interface{}) + systemAttributesMap := make(map[string]int64) + for idx, value := range c.options.SystemAttributes { + systemAttributesMap[value] = convert2Int64(systemAttributesValue[idx]) + } + processData["sys_perf"] = systemAttributesMap +} + +func (c *perfdSysmontap) parseSystemData(dataArray []interface{}) { + timestamp := time.Now().Unix() + var systemInfo map[string]interface{} + data1 := dataArray[0].(map[string]interface{}) + data2 := dataArray[1].(map[string]interface{}) + if _, ok := data1["SystemCPUUsage"]; ok { + systemInfo = data1 + } else { + systemInfo = data2 + } + + // systemInfo example: + // map[ + // CPUCount:2 + // EnabledCPUs:2 + // PerCPUUsage:[ + // map[CPU_NiceLoad:0 CPU_SystemLoad:-1 CPU_TotalLoad:3.9215686274509807 CPU_UserLoad:-1] + // map[CPU_NiceLoad:0 CPU_SystemLoad:-1 CPU_TotalLoad:11.650485436893206 CPU_UserLoad:-1]] + // ] + // System:[704211 35486281728 6303789056 3001119 1001 11033 52668 1740 40022 2114 17310 126903 1835008 160323 107909856 95067 95808179] + // SystemCPUUsage:map[ + // CPU_NiceLoad:0 + // CPU_SystemLoad:-1 + // CPU_TotalLoad:15.572054064344186 + // CPU_UserLoad:-1 + // ] + // StartMachAbsTime:5339240248449 + // EndMachAbsTime:5339264441260 + // Type:41 + // ] + + if c.options.SysCPU { + sysCPUUsage := systemInfo["SystemCPUUsage"].(map[string]interface{}) + sysCPUInfo := SystemCPUData{ + PerfDataBase: PerfDataBase{ + Type: "sys_cpu", + TimeStamp: timestamp, + }, + NiceLoad: sysCPUUsage["CPU_NiceLoad"].(float64), + SystemLoad: sysCPUUsage["CPU_SystemLoad"].(float64), + TotalLoad: sysCPUUsage["CPU_TotalLoad"].(float64), + UserLoad: sysCPUUsage["CPU_UserLoad"].(float64), + } + cpuBytes, _ := json.Marshal(sysCPUInfo) + c.chanSysCPU <- cpuBytes + } + + systemAttributesValue := systemInfo["System"].([]interface{}) + systemAttributesMap := make(map[string]int64) + for idx, value := range c.options.SystemAttributes { + systemAttributesMap[value] = convert2Int64(systemAttributesValue[idx]) + } + + if c.options.SysMem { + kernelPageSize := int64(1) // why 16384 ? + appMemory := (systemAttributesMap["vmIntPageCount"] - systemAttributesMap["vmPurgeableCount"]) * kernelPageSize + cachedFiles := (systemAttributesMap["vmExtPageCount"] - systemAttributesMap["vmPurgeableCount"]) * kernelPageSize + compressed := systemAttributesMap["vmCompressorPageCount"] * kernelPageSize + usedMemory := (systemAttributesMap["vmUsedCount"] - systemAttributesMap["vmExtPageCount"]) * kernelPageSize + wiredMemory := systemAttributesMap["vmWireCount"] * kernelPageSize + swapUsed := systemAttributesMap["__vmSwapUsage"] + freeMemory := systemAttributesMap["vmFreeCount"] * kernelPageSize + + sysMemInfo := SystemMemData{ + PerfDataBase: PerfDataBase{ + Type: "sys_mem", + TimeStamp: timestamp, + }, + AppMemory: appMemory, + UsedMemory: usedMemory, + WiredMemory: wiredMemory, + FreeMemory: freeMemory, + CachedFiles: cachedFiles, + Compressed: compressed, + SwapUsed: swapUsed, + } + memBytes, _ := json.Marshal(sysMemInfo) + c.chanSysMem <- memBytes + } + + if c.options.SysDisk { + diskBytesRead := systemAttributesMap["diskBytesRead"] + diskBytesWritten := systemAttributesMap["diskBytesWritten"] + diskReadOps := systemAttributesMap["diskReadOps"] + diskWriteOps := systemAttributesMap["diskWriteOps"] + + sysDiskInfo := SystemDiskData{ + PerfDataBase: PerfDataBase{ + Type: "sys_disk", + TimeStamp: timestamp, + }, + DataRead: diskBytesRead, + DataWritten: diskBytesWritten, + ReadOps: diskReadOps, + WriteOps: diskWriteOps, + } + diskBytes, _ := json.Marshal(sysDiskInfo) + c.chanSysDisk <- diskBytes + } + + if c.options.SysNetwork { + netBytesIn := systemAttributesMap["netBytesIn"] + netBytesOut := systemAttributesMap["netBytesOut"] + netPacketsIn := systemAttributesMap["netPacketsIn"] + netPacketsOut := systemAttributesMap["netPacketsOut"] + + sysNetworkInfo := SystemNetworkData{ + PerfDataBase: PerfDataBase{ + Type: "sys_network", + TimeStamp: timestamp, + }, + BytesIn: netBytesIn, + BytesOut: netBytesOut, + PacketsIn: netPacketsIn, + PacketsOut: netPacketsOut, + } + networkBytes, _ := json.Marshal(sysNetworkInfo) + c.chanSysNetwork <- networkBytes + } +} + +type SystemCPUData struct { + PerfDataBase // system cpu + NiceLoad float64 `json:"nice_load"` + SystemLoad float64 `json:"system_load"` + TotalLoad float64 `json:"total_load"` + UserLoad float64 `json:"user_load"` +} + +type SystemMemData struct { + PerfDataBase // mem + AppMemory int64 `json:"app_memory"` + FreeMemory int64 `json:"free_memory"` + UsedMemory int64 `json:"used_memory"` + WiredMemory int64 `json:"wired_memory"` + CachedFiles int64 `json:"cached_files"` + Compressed int64 `json:"compressed"` + SwapUsed int64 `json:"swap_used"` +} + +type SystemDiskData struct { + PerfDataBase // disk + DataRead int64 `json:"data_read"` + DataWritten int64 `json:"data_written"` + ReadOps int64 `json:"reads_in"` + WriteOps int64 `json:"writes_out"` +} + +type SystemNetworkData struct { + PerfDataBase // network + BytesIn int64 `json:"bytes_in"` + BytesOut int64 `json:"bytes_out"` + PacketsIn int64 `json:"packets_in"` + PacketsOut int64 `json:"packets_out"` +} + +func (d *device) newPerfdNetworking(options *PerfOptions) (*perfdNetworking, error) { + instruments, err := d.newInstrumentsService() + if err != nil { + return nil, err + } + return &perfdNetworking{ + perfdClient: perfdClient{ + i: instruments, + options: options, + stop: make(chan struct{}), + }, + chanNetwork: make(chan []byte, 10), + }, nil +} + +type perfdNetworking struct { + perfdClient + chanNetwork chan []byte // network channel +} + +func (c *perfdNetworking) Start() (data <-chan []byte, err error) { + if _, err = c.i.call( + instrumentsServiceNetworking, + "replayLastRecordedSession", + ); err != nil { + return nil, err + } + + if _, err = c.i.call( + instrumentsServiceNetworking, + "startMonitoring", + ); err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.TODO()) + c.i.registerCallback("", func(m libimobiledevice.DTXMessageResult) { + select { + case <-ctx.Done(): + c.i.call(instrumentsServiceNetworking, "stopMonitoring") + return + default: + c.parseNetworking(m.Obj) + } + }) + c.cancel = cancel + + outCh := make(chan []byte, 100) + + go func() { + for { + select { + case <-c.stop: + c.cancel() + return + case networkBytes, ok := <-c.chanNetwork: + if ok { + outCh <- networkBytes + } + } + } + }() + + return outCh, nil +} + +func (c *perfdNetworking) Stop() { + close(c.stop) +} + +func (c *perfdNetworking) parseNetworking(data interface{}) { + raw, ok := data.([]interface{}) + if !ok || len(raw) != 2 { + fmt.Printf("invalid networking data: %v\n", data) + return + } + + var netBytes []byte + msgType := raw[0].(uint64) + msgValue := raw[1].([]interface{}) + if msgType == 0 { + // interface-detection + // ['InterfaceIndex', "Name"] + // e.g. [0, [14, 'en0']] + netData := NetworkDataInterfaceDetection{ + PerfDataBase: PerfDataBase{ + Type: "network-interface-detection", + TimeStamp: time.Now().Unix(), + }, + InterfaceIndex: convert2Int64(msgValue[0]), + Name: msgValue[1].(string), + } + netBytes, _ = json.Marshal(netData) + } else if msgType == 1 { + // connection-detected + // ['LocalAddress', 'RemoteAddress', 'InterfaceIndex', 'Pid', + // 'RecvBufferSize', 'RecvBufferUsed', 'SerialNumber', 'Kind'] + // e.g. [1 [[16 2 211 158 192 168 100 101 0 0 0 0 0 0 0 0] + // [16 2 0 53 183 221 253 100 0 0 0 0 0 0 0 0] + // 14 -2 786896 0 133 2]] + + localAddr, err := parseSocketAddr(msgValue[0].([]byte)) + if err != nil { + fmt.Printf("parse local socket address err: %v\n", err) + } + remoteAddr, err := parseSocketAddr(msgValue[1].([]byte)) + if err != nil { + fmt.Printf("parse remote socket address err: %v\n", err) + } + netData := NetworkDataConnectionDetected{ + PerfDataBase: PerfDataBase{ + Type: "network-connection-detected", + TimeStamp: time.Now().Unix(), + }, + LocalAddress: localAddr, + RemoteAddress: remoteAddr, + InterfaceIndex: convert2Int64(msgValue[2]), + Pid: convert2Int64(msgValue[3]), + RecvBufferSize: convert2Int64(msgValue[4]), + RecvBufferUsed: convert2Int64(msgValue[5]), + SerialNumber: convert2Int64(msgValue[6]), + Kind: convert2Int64(msgValue[7]), + } + netBytes, _ = json.Marshal(netData) + } else if msgType == 2 { + // connection-update + // ['RxPackets', 'RxBytes', 'TxPackets', 'TxBytes', + // 'RxDups', 'RxOOO', 'TxRetx', 'MinRTT', 'AvgRTT', 'ConnectionSerial'] + // e.g. [2, [21, 1708, 22, 14119, 309, 0, 5830, 0.076125, 0.076125, 54, -1]] + netData := NetworkDataConnectionUpdate{ + PerfDataBase: PerfDataBase{ + Type: "network-connection-update", + TimeStamp: time.Now().Unix(), + }, + RxBytes: convert2Int64(msgValue[0]), + RxPackets: convert2Int64(msgValue[1]), + TxBytes: convert2Int64(msgValue[2]), + TxPackets: convert2Int64(msgValue[3]), + } + if value, ok := msgValue[4].(uint64); ok { + netData.RxDups = int64(value) + } + if value, ok := msgValue[5].(uint64); ok { + netData.RxOOO = int64(value) + } + if value, ok := msgValue[6].(uint64); ok { + netData.TxRetx = int64(value) + } + if value, ok := msgValue[7].(uint64); ok { + netData.MinRTT = int64(value) + } + if value, ok := msgValue[8].(uint64); ok { + netData.AvgRTT = int64(value) + } + if value, ok := msgValue[9].(uint64); ok { + netData.ConnectionSerial = int64(value) + } + + netBytes, _ = json.Marshal(netData) + } + c.chanNetwork <- netBytes +} + +func parseSocketAddr(data []byte) (string, error) { + len := data[0] // length of address + _ = data[1] // family + port := binary.BigEndian.Uint16(data[2:4]) // port + + // network, data[4:4+len] + if len == 0x10 { + // IPv4, 4 bytes + ip := net.IP(data[4:8]) + return fmt.Sprintf("%s:%d", ip, port), nil + } else if len == 0x1c { + // IPv6, 16 bytes + ip := net.IP(data[4:20]) + return fmt.Sprintf("%s:%d", ip, port), nil + } + return "", fmt.Errorf("invalid socket address: %v", data) +} + +type PerfDataBase struct { + Type string `json:"type"` + TimeStamp int64 `json:"timestamp"` + Msg string `json:"msg,omitempty"` // message for invalid data +} + +// network-interface-detection +type NetworkDataInterfaceDetection struct { + PerfDataBase + InterfaceIndex int64 `json:"interface_index"` // 0 + Name string `json:"name"` // 1 +} + +// network-connection-detected +type NetworkDataConnectionDetected struct { + PerfDataBase + LocalAddress string `json:"local_address"` // 0 + RemoteAddress string `json:"remote_address"` // 1 + InterfaceIndex int64 `json:"interface_index"` // 2 + Pid int64 `json:"pid"` // 3 + RecvBufferSize int64 `json:"recv_buffer_size"` // 4 + RecvBufferUsed int64 `json:"recv_buffer_used"` // 5 + SerialNumber int64 `json:"serial_number"` // 6 + Kind int64 `json:"kind"` // 7 +} + +// network-connection-update +type NetworkDataConnectionUpdate struct { + PerfDataBase + RxBytes int64 `json:"rx_bytes"` // 0 + RxPackets int64 `json:"rx_packets"` // 1 + TxBytes int64 `json:"tx_bytes"` // 2 + TxPackets int64 `json:"tx_packets"` // 3 + RxDups int64 `json:"rx_dups,omitempty"` // 4 + RxOOO int64 `json:"rx_000,omitempty"` // 5 + TxRetx int64 `json:"tx_retx,omitempty"` // 6 + MinRTT int64 `json:"min_rtt,omitempty"` // 7 + AvgRTT int64 `json:"avg_rtt,omitempty"` // 8 + ConnectionSerial int64 `json:"connection_serial"` // 9 +} + +func (d *device) newPerfdGraphicsOpengl(options *PerfOptions) (*perfdGraphicsOpengl, error) { + instruments, err := d.newInstrumentsService() + if err != nil { + return nil, err + } + return &perfdGraphicsOpengl{ + perfdClient: perfdClient{ + i: instruments, + options: options, + stop: make(chan struct{}), + }, + chanGPU: make(chan []byte, 10), + chanFPS: make(chan []byte, 10), + }, nil +} + +type perfdGraphicsOpengl struct { + perfdClient + chanGPU chan []byte // gpu channel + chanFPS chan []byte // fps channel +} + +func (c *perfdGraphicsOpengl) Start() (data <-chan []byte, err error) { + if _, err = c.i.call( + instrumentsServiceGraphicsOpengl, + "setSamplingRate:", + float64(c.options.OutputInterval)/100, + ); err != nil { + return nil, err + } + + if _, err = c.i.call( + instrumentsServiceGraphicsOpengl, + "startSamplingAtTimeInterval:", + 0, + ); err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.TODO()) + c.i.registerCallback("", func(m libimobiledevice.DTXMessageResult) { + select { + case <-ctx.Done(): + c.i.call(instrumentsServiceGraphicsOpengl, "stopSampling") + return + default: + c.parseData(m.Obj) + } + }) + c.cancel = cancel + + outCh := make(chan []byte, 100) + + go func() { + for { + select { + case <-c.stop: + c.cancel() + return + case gpuBytes, ok := <-c.chanGPU: + if ok { + outCh <- gpuBytes + } + case fpsBytes, ok := <-c.chanFPS: + if ok { + outCh <- fpsBytes + } + } + } + }() + + return outCh, nil +} + +func (c *perfdGraphicsOpengl) Stop() { + close(c.stop) +} + +func (c *perfdGraphicsOpengl) parseData(data interface{}) { + // data example: + // map[ + // Alloc system memory:50167808 + // Allocated PB Size:1179648 + // CoreAnimationFramesPerSecond:0 // fps from GPU + // Device Utilization %:0 // device + // IOGLBundleName:Built-In + // In use system memory:10633216 + // Renderer Utilization %:0 // renderer + // SplitSceneCount:0 + // TiledSceneBytes:0 + // Tiler Utilization %:0 // tiler + // XRVideoCardRunTimeStamp:1010679 + // recoveryCount:0 + // ] + + gpuInfo := GPUData{ + PerfDataBase: PerfDataBase{ + Type: "gpu", + TimeStamp: time.Now().Unix(), + }, + } + fpsInfo := FPSData{ + PerfDataBase: PerfDataBase{ + Type: "fps", + TimeStamp: time.Now().Unix(), + }, + } + + defer func() { + if c.options.gpu { + gpuBytes, _ := json.Marshal(gpuInfo) + c.chanGPU <- gpuBytes + } + if c.options.FPS { + fpsBytes, _ := json.Marshal(fpsInfo) + c.chanFPS <- fpsBytes + } + }() + + raw, ok := data.(map[string]interface{}) + if !ok { + gpuInfo.Msg = fmt.Sprintf("invalid graphics.opengl data: %v", data) + return + } + + // gpu + gpuInfo.DeviceUtilization = convert2Int64(raw["Device Utilization %"]) + gpuInfo.TilerUtilization = convert2Int64(raw["Tiler Utilization %"]) + gpuInfo.RendererUtilization = convert2Int64(raw["Renderer Utilization %"]) + + // fps + fpsInfo.FPS = int(convert2Int64(raw["CoreAnimationFramesPerSecond"])) +} + +type GPUData struct { + PerfDataBase // gpu + TilerUtilization int64 `json:"tiler_utilization"` // 处理顶点的 GPU 时间占比 + DeviceUtilization int64 `json:"device_utilization"` // 设备利用率 + RendererUtilization int64 `json:"renderer_utilization"` // 渲染器利用率 +} + +type FPSData struct { + PerfDataBase // fps + FPS int `json:"fps"` +} + +func convert2Int64(num interface{}) int64 { + switch value := num.(type) { + case int64: + return value + case uint64: + return int64(value) + case uint32: + return int64(value) + case uint16: + return int64(value) + case uint8: + return int64(value) + case uint: + return int64(value) + } + fmt.Printf("convert2Int64 failed: %v, %T\n", num, num) + return -1 +} + +func containString(ss []string, s string) bool { + for _, v := range ss { + if s == v { + return true + } + } + return false +} diff --git a/hrp/pkg/gidevice/perfd_test.go b/hrp/pkg/gidevice/perfd_test.go new file mode 100644 index 00000000..7e07849a --- /dev/null +++ b/hrp/pkg/gidevice/perfd_test.go @@ -0,0 +1,178 @@ +package gidevice + +import ( + "fmt" + "testing" + "time" +) + +func TestPerfSystemMonitor(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PerfStart( + WithPerfSystemCPU(true), + WithPerfSystemMem(true), + WithPerfSystemDisk(true), + WithPerfSystemNetwork(true), + WithPerfOutputInterval(1000), + ) + if err != nil { + t.Fatal(err) + } + + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPerfProcessMonitor(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PerfStart( + WithPerfProcessAttributes("cpuUsage", "memAnon"), + WithPerfOutputInterval(1000), + WithPerfPID(100), + WithPerfBundleID("com.apple.mobilesafari"), // higher priority than pid + ) + if err != nil { + t.Fatal(err) + } + + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPerfGPU(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PerfStart( + WithPerfSystemCPU(false), + WithPerfSystemMem(false), + WithPerfGPU(true), + ) + if err != nil { + t.Fatal(err) + } + + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPerfFPS(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PerfStart( + WithPerfSystemCPU(false), + WithPerfSystemMem(false), + WithPerfFPS(true), + ) + if err != nil { + t.Fatal(err) + } + + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPerfNetwork(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PerfStart( + WithPerfSystemCPU(false), + WithPerfSystemMem(false), + WithPerfNetwork(true), + ) + if err != nil { + t.Fatal(err) + } + + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPerfAll(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PerfStart( + WithPerfSystemCPU(true), + WithPerfSystemMem(true), + WithPerfSystemDisk(true), + WithPerfSystemNetwork(true), + WithPerfNetwork(true), + WithPerfFPS(true), + WithPerfGPU(true), + WithPerfBundleID("com.apple.mobilesafari"), + ) + if err != nil { + t.Fatal(err) + } + + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPcap(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.Pcap() + if err != nil { + t.Fatal(err) + } + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PcapStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} diff --git a/hrp/pkg/gidevice/pkg/ipa/ipa.go b/hrp/pkg/gidevice/pkg/ipa/ipa.go new file mode 100644 index 00000000..52aa1961 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/ipa/ipa.go @@ -0,0 +1,53 @@ +package ipa + +import ( + "archive/zip" + "fmt" + "io" + "path" + + "howett.net/plist" +) + +func Info(ipaPath string) (info map[string]interface{}, err error) { + var reader *zip.ReadCloser + if reader, err = zip.OpenReader(ipaPath); err != nil { + return nil, err + } + + defer func() { + err = reader.Close() + }() + + for _, file := range reader.File { + matched, _err := path.Match("Payload/*.app/Info.plist", file.Name) + if _err != nil { + err = _err + continue + } + if !matched { + continue + } + + var rd io.ReadCloser + if rd, _err = file.Open(); _err != nil { + return nil, _err + } + data, _err := io.ReadAll(rd) + if _err != nil { + return nil, _err + } + + info = make(map[string]interface{}) + _, _err = plist.Unmarshal(data, &info) + if _err != nil { + return nil, _err + } + } + + if err != nil && len(info) == 0 { + return nil, fmt.Errorf("find Info.plist: %w", err) + } + + return +} diff --git a/hrp/pkg/gidevice/pkg/ipa/ipa_test.go b/hrp/pkg/gidevice/pkg/ipa/ipa_test.go new file mode 100644 index 00000000..10474b4d --- /dev/null +++ b/hrp/pkg/gidevice/pkg/ipa/ipa_test.go @@ -0,0 +1,21 @@ +package ipa + +import ( + "testing" +) + +func TestInfo(t *testing.T) { + name := "/Users/hero/Documents/Workspace/GitHub/taobao-iphone-device/tests/testdata/WebDriverAgentRunner.ipa" + name = "/private/tmp/derivedDataPath/Build/Products/Release-iphoneos/WebDriverAgentRunner-Runner.ipa" + + info, err := Info(name) + if err != nil { + t.Fatal(err) + } + + for k, v := range info { + t.Logf("%-50s\t%v", k, v) + } + + t.Log(info["CFBundleIdentifier"]) +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/afc.go b/hrp/pkg/gidevice/pkg/libimobiledevice/afc.go new file mode 100644 index 00000000..d9fac44b --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/afc.go @@ -0,0 +1,105 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/hex" + "fmt" +) + +const AfcServiceName = "com.apple.afc" + +func NewAfcClient(innerConn InnerConn) *AfcClient { + return &AfcClient{ + innerConn: innerConn, + } +} + +type AfcClient struct { + innerConn InnerConn + packetNum uint64 +} + +func (c *AfcClient) newPacket(operation uint64, data, payload []byte) Packet { + c.packetNum++ + pkt := &afcPacket{ + operation: operation, + packetNum: c.packetNum, + entireLen: 40, + thisLen: 40, + } + if data != nil { + n := uint64(len(data)) + pkt.entireLen += n + pkt.thisLen += n + } + if payload != nil { + pkt.entireLen += uint64(len(payload)) + } + return pkt +} + +func (c *AfcClient) Send(operation uint64, data, payload []byte) (err error) { + pkt := c.newPacket(operation, data, payload) + var raw []byte + if raw, err = pkt.Pack(); err != nil { + return fmt.Errorf("send packet (afc): %w", err) + } + + buf := new(bytes.Buffer) + buf.Write(raw) + if data != nil { + debugLog(fmt.Sprintf("--> %s ...afc data...\n", pkt)) + buf.Write(data) + } else { + debugLog(fmt.Sprintf("--> %s\n", pkt)) + } + + if err = c.innerConn.Write(buf.Bytes()); err != nil { + return fmt.Errorf("send packet (afc): %w", err) + } + + if payload != nil { + if err = c.innerConn.Write(payload); err != nil { + return fmt.Errorf("send packet (afc): %w", err) + } + } + + return +} + +func (c *AfcClient) Receive() (respMsg *AfcMessage, err error) { + var bufHeader []byte + if bufHeader, err = c.innerConn.Read(40); err != nil { + return nil, fmt.Errorf("receive packet (afc): %w", err) + } + buffer := new(bytes.Buffer) + buffer.Write(bufHeader) + var respPkt *afcPacket + if respPkt, err = new(afcPacket).unpack(buffer); err != nil { + return nil, fmt.Errorf("receive packet (afc): %w", err) + } + + respMsg = new(AfcMessage) + respMsg.Operation = respPkt.operation + + buffer.Reset() + if respPkt.entireLen > 40 { + length := int(respPkt.entireLen - 40) + var bufDataAndPayload []byte + if bufDataAndPayload, err = c.innerConn.Read(length); err != nil { + return nil, fmt.Errorf("receive packet (afc): %w", err) + } + buffer.Write(bufDataAndPayload) + } + + bufData := make([]byte, respPkt.thisLen-40) + if _, err = buffer.Read(bufData); err != nil { + return nil, fmt.Errorf("receive packet (afc buffer): %w", err) + } + respMsg.Data = bufData + respMsg.Payload = buffer.Bytes() + + debugLog(fmt.Sprintf("<-- %s\n%s\n%s", respPkt, hex.Dump(respMsg.Data), hex.Dump(respMsg.Payload))) + + return +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/afcmessage.go b/hrp/pkg/gidevice/pkg/libimobiledevice/afcmessage.go new file mode 100644 index 00000000..adcd7caf --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/afcmessage.go @@ -0,0 +1,187 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "errors" +) + +type AfcMessage struct { + Operation uint64 + Data []byte + Payload []byte +} + +func (m *AfcMessage) Map() map[string]string { + ret := make(map[string]string) + ss := m.Strings() + if ss != nil { + for i := 0; i < len(ss); i += 2 { + ret[ss[i]] = ss[i+1] + } + } + return ret +} + +func (m *AfcMessage) Strings() []string { + if m.Operation == AfcOperationData { + bs := bytes.Split(m.Payload, []byte{0}) + ss := make([]string, len(bs)-1) + for i := 0; i < len(ss); i++ { + ss[i] = string(bs[i]) + } + return ss + } + return nil +} + +func (m *AfcMessage) Uint64() uint64 { + return binary.LittleEndian.Uint64(m.Data) +} + +func (m *AfcMessage) Err() error { + if m.Operation == AfcOperationStatus { + status := m.Uint64() + if status != AfcErrSuccess { + return toError(status) + } + } + return nil +} + +func toError(status uint64) error { + switch status { + case AfcErrUnknownError: + return errors.New("UnknownError") + case AfcErrOperationHeaderInvalid: + return errors.New("OperationHeaderInvalid") + case AfcErrNoResources: + return errors.New("NoResources") + case AfcErrReadError: + return errors.New("ReadError") + case AfcErrWriteError: + return errors.New("WriteError") + case AfcErrUnknownPacketType: + return errors.New("UnknownPacketType") + case AfcErrInvalidArgument: + return errors.New("InvalidArgument") + case AfcErrObjectNotFound: + return errors.New("ObjectNotFound") + case AfcErrObjectIsDir: + return errors.New("ObjectIsDir") + case AfcErrPermDenied: + return errors.New("PermDenied") + case AfcErrServiceNotConnected: + return errors.New("ServiceNotConnected") + case AfcErrOperationTimeout: + return errors.New("OperationTimeout") + case AfcErrTooMuchData: + return errors.New("TooMuchData") + case AfcErrEndOfData: + return errors.New("EndOfData") + case AfcErrOperationNotSupported: + return errors.New("OperationNotSupported") + case AfcErrObjectExists: + return errors.New("ObjectExists") + case AfcErrObjectBusy: + return errors.New("ObjectBusy") + case AfcErrNoSpaceLeft: + return errors.New("NoSpaceLeft") + case AfcErrOperationWouldBlock: + return errors.New("OperationWouldBlock") + case AfcErrIoError: + return errors.New("IoError") + case AfcErrOperationInterrupted: + return errors.New("OperationInterrupted") + case AfcErrOperationInProgress: + return errors.New("OperationInProgress") + case AfcErrInternalError: + return errors.New("InternalError") + case AfcErrMuxError: + return errors.New("MuxError") + case AfcErrNoMemory: + return errors.New("NoMemory") + case AfcErrNotEnoughData: + return errors.New("NotEnoughData") + case AfcErrDirNotEmpty: + return errors.New("DirNotEmpty") + } + return nil +} + +const ( + AfcOperationInvalid = 0x00000000 /* Invalid */ + AfcOperationStatus = 0x00000001 /* Status */ + AfcOperationData = 0x00000002 /* Data */ + AfcOperationReadDir = 0x00000003 /* ReadDir */ + AfcOperationReadFile = 0x00000004 /* ReadFile */ + AfcOperationWriteFile = 0x00000005 /* WriteFile */ + AfcOperationWritePart = 0x00000006 /* WritePart */ + AfcOperationTruncateFile = 0x00000007 /* TruncateFile */ + AfcOperationRemovePath = 0x00000008 /* RemovePath */ + AfcOperationMakeDir = 0x00000009 /* MakeDir */ + AfcOperationGetFileInfo = 0x0000000A /* GetFileInfo */ + AfcOperationGetDeviceInfo = 0x0000000B /* GetDeviceInfo */ + AfcOperationWriteFileAtomic = 0x0000000C /* WriteFileAtomic (tmp file+rename) */ + AfcOperationFileOpen = 0x0000000D /* FileRefOpen */ + AfcOperationFileOpenResult = 0x0000000E /* FileRefOpenResult */ + AfcOperationFileRead = 0x0000000F /* FileRefRead */ + AfcOperationFileWrite = 0x00000010 /* FileRefWrite */ + AfcOperationFileSeek = 0x00000011 /* FileRefSeek */ + AfcOperationFileTell = 0x00000012 /* FileRefTell */ + AfcOperationFileTellResult = 0x00000013 /* FileRefTellResult */ + AfcOperationFileClose = 0x00000014 /* FileRefClose */ + AfcOperationFileSetSize = 0x00000015 /* FileRefSetFileSize (ftruncate) */ + AfcOperationGetConnectionInfo = 0x00000016 /* GetConnectionInfo */ + AfcOperationSetConnectionOptions = 0x00000017 /* SetConnectionOptions */ + AfcOperationRenamePath = 0x00000018 /* RenamePath */ + AfcOperationSetFSBlockSize = 0x00000019 /* SetFSBlockSize (0x800000) */ + AfcOperationSetSocketBlockSize = 0x0000001A /* SetSocketBlockSize (0x800000) */ + AfcOperationFileRefLock = 0x0000001B /* FileRefLock */ + AfcOperationMakeLink = 0x0000001C /* MakeLink */ + AfcOperationGetFileHash = 0x0000001D /* GetFileHash */ + AfcOperationSetFileModTime = 0x0000001E /* SetModTime */ + AfcOperationGetFileHashRange = 0x0000001F /* GetFileHashWithRange */ + /* iOS 6+ */ + AfcOperationFileSetImmutableHint = 0x00000020 /* FileRefSetImmutableHint */ + AfcOperationGetSizeOfPathContents = 0x00000021 /* GetSizeOfPathContents */ + AfcOperationRemovePathAndContents = 0x00000022 /* RemovePathAndContents */ + AfcOperationDirectoryEnumeratorRefOpen = 0x00000023 /* DirectoryEnumeratorRefOpen */ + AfcOperationDirectoryEnumeratorRefOpenResult = 0x00000024 /* DirectoryEnumeratorRefOpenResult */ + AfcOperationDirectoryEnumeratorRefRead = 0x00000025 /* DirectoryEnumeratorRefRead */ + AfcOperationDirectoryEnumeratorRefClose = 0x00000026 /* DirectoryEnumeratorRefClose */ + /* iOS 7+ */ + AfcOperationFileRefReadWithOffset = 0x00000027 /* FileRefReadWithOffset */ + AfcOperationFileRefWriteWithOffset = 0x00000028 /* FileRefWriteWithOffset */ +) + +const ( + AfcErrSuccess = 0 + AfcErrUnknownError = 1 + AfcErrOperationHeaderInvalid = 2 + AfcErrNoResources = 3 + AfcErrReadError = 4 + AfcErrWriteError = 5 + AfcErrUnknownPacketType = 6 + AfcErrInvalidArgument = 7 + AfcErrObjectNotFound = 8 + AfcErrObjectIsDir = 9 + AfcErrPermDenied = 10 + AfcErrServiceNotConnected = 11 + AfcErrOperationTimeout = 12 + AfcErrTooMuchData = 13 + AfcErrEndOfData = 14 + AfcErrOperationNotSupported = 15 + AfcErrObjectExists = 16 + AfcErrObjectBusy = 17 + AfcErrNoSpaceLeft = 18 + AfcErrOperationWouldBlock = 19 + AfcErrIoError = 20 + AfcErrOperationInterrupted = 21 + AfcErrOperationInProgress = 22 + AfcErrInternalError = 23 + AfcErrMuxError = 30 + AfcErrNoMemory = 31 + AfcErrNotEnoughData = 32 + AfcErrDirNotEmpty = 33 +) diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/auxbuffer.go b/hrp/pkg/gidevice/pkg/libimobiledevice/auxbuffer.go new file mode 100644 index 00000000..cd1181fa --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/auxbuffer.go @@ -0,0 +1,138 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "errors" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/nskeyedarchiver" +) + +type AuxBuffer struct { + buf *bytes.Buffer +} + +func NewAuxBuffer() *AuxBuffer { + return &AuxBuffer{ + buf: new(bytes.Buffer), + } +} + +func (m *AuxBuffer) AppendObject(obj interface{}) error { + marshal, err := nskeyedarchiver.Marshal(obj) + if err != nil { + return err + } + m.AppendUInt32(10) + m.AppendUInt32(2) + m.AppendUInt32(uint32(len(marshal))) + m.buf.Write(marshal) + + return nil +} + +func (m *AuxBuffer) AppendInt64(v int64) { + m.AppendUInt32(10) + m.AppendUInt32(4) + m.AppendUInt64(uint64(v)) +} + +func (m *AuxBuffer) AppendInt32(v int32) { + m.AppendUInt32(10) + m.AppendUInt32(3) + m.AppendUInt32(uint32(v)) +} + +func (m *AuxBuffer) AppendUInt32(v uint32) { + _ = binary.Write(m.buf, binary.LittleEndian, v) +} + +func (m *AuxBuffer) AppendUInt64(v uint64) { + _ = binary.Write(m.buf, binary.LittleEndian, v) +} + +func (m *AuxBuffer) AppendBytes(b []byte) { + m.buf.Write(b) +} + +func (m *AuxBuffer) Len() int { + return m.buf.Len() +} + +func (m *AuxBuffer) Bytes() []byte { + dup := m.buf.Bytes() + b := make([]byte, 16) + binary.LittleEndian.PutUint64(b, 0x01f0) + binary.LittleEndian.PutUint64(b[8:], uint64(m.Len())) + return append(b, dup...) +} + +func UnmarshalAuxBuffer(b []byte) ([]interface{}, error) { + reader := bytes.NewReader(b) + var magic, pkgLen uint64 + if err := binary.Read(reader, binary.LittleEndian, &magic); err != nil { + return nil, err + } + if err := binary.Read(reader, binary.LittleEndian, &pkgLen); err != nil { + return nil, err + } + + // if magic != 0x1df0 { + // TODO magic + // return nil, errors.New("magic not equal 0x1df0") + // } + + if pkgLen > uint64(len(b)-16) { + return nil, errors.New("package length not enough") + } + + var ret []interface{} + + for reader.Len() > 0 { + var flag, typ uint32 + if err := binary.Read(reader, binary.LittleEndian, &flag); err != nil { + return nil, err + } + if err := binary.Read(reader, binary.LittleEndian, &typ); err != nil { + return nil, err + } + switch typ { + case 2: + var l uint32 + if err := binary.Read(reader, binary.LittleEndian, &l); err != nil { + return nil, err + } + plistBuf := make([]byte, l) + if _, err := reader.Read(plistBuf); err != nil { + return nil, err + } + archiver := NewNSKeyedArchiver() + d, err := archiver.Unmarshal(plistBuf) + if err != nil { + return nil, err + } + ret = append(ret, d) + case 3, 5: + var i int32 + if err := binary.Read(reader, binary.LittleEndian, &i); err != nil { + return nil, err + } + ret = append(ret, i) + case 4, 6: + var i int64 + if err := binary.Read(reader, binary.LittleEndian, &i); err != nil { + return nil, err + } + ret = append(ret, i) + case 10: + // TODO Dictionary key + // fmt.Println("Dictionary key!") + continue + default: + // fmt.Printf("unknown type %d\n", typ) + break + } + } + + return ret, nil +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/client_dtxmessage.go b/hrp/pkg/gidevice/pkg/libimobiledevice/client_dtxmessage.go new file mode 100644 index 00000000..6f329acd --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/client_dtxmessage.go @@ -0,0 +1,405 @@ +package libimobiledevice + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "io" + "strings" + "sync" + "time" + "unsafe" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/nskeyedarchiver" +) + +const ( + _unregistered = "_Golang-iDevice_Unregistered" + _over = "_Golang-iDevice_Over" +) + +func newDtxMessageClient(innerConn InnerConn) *dtxMessageClient { + c := &dtxMessageClient{ + innerConn: innerConn, + msgID: 0, + publishedChannels: make(map[string]int32), + openedChannels: make(map[string]uint32), + toReply: make(chan *dtxMessageHeaderPacket), + + mu: sync.Mutex{}, + resultMap: make(map[interface{}]*DTXMessageResult), + + callbackMap: make(map[string]func(m DTXMessageResult)), + } + c.RegisterCallback(_unregistered, func(m DTXMessageResult) {}) + c.RegisterCallback(_over, func(m DTXMessageResult) {}) + c.ctx, c.cancelFunc = context.WithCancel(context.Background()) + c.startReceive() + c.startWaitingForReply() + return c +} + +type dtxMessageClient struct { + innerConn InnerConn + msgID uint32 + + publishedChannels map[string]int32 + openedChannels map[string]uint32 + + toReply chan *dtxMessageHeaderPacket + + mu sync.Mutex + resultMap map[interface{}]*DTXMessageResult + + callbackMap map[string]func(m DTXMessageResult) + + ctx context.Context + cancelFunc context.CancelFunc +} + +func (c *dtxMessageClient) SendDTXMessage(selector string, aux []byte, channelCode uint32, expectsReply bool) (msgID uint32, err error) { + payload := new(dtxMessagePayloadPacket) + header := &dtxMessageHeaderPacket{ + ExpectsReply: 1, + } + + flag := 0x1000 + if !expectsReply { + flag = 0 + header.ExpectsReply = 0 + } + + var sel []byte + if sel, err = nskeyedarchiver.Marshal(selector); err != nil { + return 0, err + } + + if aux == nil { + aux = make([]byte, 0) + } + + payload.Flags = uint32(0x2 | flag) + payload.AuxiliaryLength = uint32(len(aux)) + payload.TotalLength = uint64(len(aux)) + uint64(len(sel)) + + header.Magic = 0x1F3D5B79 + header.CB = uint32(unsafe.Sizeof(*header)) + header.FragmentId = 0 + header.FragmentCount = 1 + header.Length = uint32(unsafe.Sizeof(*payload)) + uint32(payload.TotalLength) + c.msgID++ + header.Identifier = c.msgID + header.ConversationIndex = 0 + header.ChannelCode = channelCode + + msgPkt := new(dtxMessagePacket) + msgPkt.Header = header + msgPkt.Payload = payload + msgPkt.Aux = aux + msgPkt.Sel = sel + + raw, err := msgPkt.Pack() + if err != nil { + return 0, err + } + + debugLog(fmt.Sprintf("--> %s\n", msgPkt)) + msgID = header.Identifier + err = c.innerConn.Write(raw) + return +} + +func (c *dtxMessageClient) ReceiveDTXMessage() (result *DTXMessageResult, err error) { + bufPayload := new(bytes.Buffer) + + var header *dtxMessageHeaderPacket = nil + var needToReply *dtxMessageHeaderPacket = nil + + for { + header = new(dtxMessageHeaderPacket) + + lenHeader := int(unsafe.Sizeof(*header)) + var bufHeader []byte + if bufHeader, err = c.innerConn.Read(lenHeader); err != nil { + return nil, fmt.Errorf("receive: length of DTXMessageHeader: %w", err) + } + + if header, err = header.unpack(bytes.NewBuffer(bufHeader)); err != nil { + return nil, fmt.Errorf("receive: DTXMessageHeader unpack: %w", err) + } + + if header.ExpectsReply == 1 { + needToReply = header + } + + if header.Magic != 0x1F3D5B79 { + return nil, fmt.Errorf("receive: bad magic %x", header.Magic) + } + + if header.ConversationIndex == 1 { + if header.Identifier != c.msgID { + return nil, fmt.Errorf("receive: except identifier %d new identifier %d", c.msgID, header.Identifier) + } + } else if header.ConversationIndex == 0 { + if header.Identifier > c.msgID { + c.msgID = header.Identifier + } + } else { + return nil, fmt.Errorf("receive: invalid conversationIndex %d", header.ConversationIndex) + } + + if header.FragmentId == 0 && header.FragmentCount > 1 { + continue + } + + var data []byte + if data, err = c.innerConn.Read(int(header.Length)); err != nil { + return nil, fmt.Errorf("receive: length of DTXMessageHeader: %w", err) + } + bufPayload.Write(data) + + if header.FragmentId == header.FragmentCount-1 { + break + } + } + + rawPayload := bufPayload.Bytes() + payload := new(dtxMessagePayloadPacket) + if payload, err = payload.unpack(bufPayload); err != nil { + return nil, fmt.Errorf("receive: unpack DTXMessagePayload: %w", err) + } + + compress := (payload.Flags & 0xff000) >> 12 + if compress != 0 { + return nil, fmt.Errorf("receive: message is compressed type %d", compress) + } + + payloadSize := uint32(unsafe.Sizeof(*payload)) + objOffset := uint64(payloadSize + payload.AuxiliaryLength) + + var aux, obj []byte + + // see https://github.com/electricbubble/gidevice/issues/28 + if r, l := payloadSize+payload.AuxiliaryLength, len(rawPayload); int(r) <= l { + aux = rawPayload[payloadSize:r] + } else { + debugLog(fmt.Sprintf("<-- DTXMessage %s\n%s\n"+ + "[aux] bounds out of range [:%d] with capacity %d", + header.String(), payload.String(), + r, l, + )) + } + if r, l := objOffset+(payload.TotalLength-uint64(payload.AuxiliaryLength)), len(rawPayload); int(r) <= l { + obj = rawPayload[objOffset:r] + } else { + debugLog(fmt.Sprintf("<-- DTXMessage %s\n%s\n"+ + "[obj] bounds out of range [:%d] with capacity %d", + header.String(), payload.String(), + r, l, + )) + } + + debugLog(fmt.Sprintf( + "<-- DTXMessage %s\n%s\n"+ + "%s\n%s\n", + header.String(), payload.String(), + hex.Dump(aux), hex.Dump(obj), + )) + + result = new(DTXMessageResult) + + if len(aux) > 0 { + if aux, err := UnmarshalAuxBuffer(aux); err != nil { + return nil, fmt.Errorf("receive: unpack AUX: %w", err) + } else { + result.Aux = aux + } + } + + if len(obj) > 0 { + if obj, err := NewNSKeyedArchiver().Unmarshal(obj); err != nil { + return nil, fmt.Errorf("receive: unpack NSKeyedArchiver: %w", err) + } else { + result.Obj = obj + } + } + + sObj, ok := result.Obj.(string) + if fn, do := c.callbackMap[sObj]; do { + fn(*result) + } else { + c.callbackMap[_unregistered](*result) + } + + if needToReply != nil { + go func() { c.toReply <- needToReply }() + } else { + var sk interface{} = header.Identifier + + if ok && sObj == "_notifyOfPublishedCapabilities:" { + sk = "_notifyOfPublishedCapabilities:" + } + c.mu.Lock() + c.resultMap[sk] = result + c.mu.Unlock() + } + + return +} + +func (c *dtxMessageClient) Connection() (publishedChannels map[string]int32, err error) { + args := NewAuxBuffer() + if err = args.AppendObject(map[string]interface{}{ + "com.apple.private.DTXBlockCompression": uint64(2), + "com.apple.private.DTXConnection": uint64(1), + }); err != nil { + return nil, fmt.Errorf("connection DTXMessage: %w", err) + } + + selector := "_notifyOfPublishedCapabilities:" + if _, err = c.SendDTXMessage(selector, args.Bytes(), 0, false); err != nil { + return nil, fmt.Errorf("connection send: %w", err) + } + + var result *DTXMessageResult + if result, err = c.GetResult(selector); err != nil { + return nil, fmt.Errorf("connection receive: %w", err) + } + + if result.Obj.(string) != "_notifyOfPublishedCapabilities:" { + return nil, fmt.Errorf("connection: response mismatch: %s", result.Obj) + } + + aux := result.Aux[0].(map[string]interface{}) + for k, v := range aux { + c.publishedChannels[k] = int32(v.(uint64)) + } + + return c.publishedChannels, nil +} + +func (c *dtxMessageClient) MakeChannel(channel string) (id uint32, err error) { + var ok bool + if id, ok = c.openedChannels[channel]; ok { + return id, nil + } + + id = uint32(len(c.openedChannels) + 1) + args := NewAuxBuffer() + args.AppendInt32(int32(id)) + if err = args.AppendObject(channel); err != nil { + return 0, fmt.Errorf("make channel DTXMessage: %w", err) + } + + selector := "_requestChannelWithCode:identifier:" + + var msgID uint32 + if msgID, err = c.SendDTXMessage(selector, args.Bytes(), 0, true); err != nil { + return 0, fmt.Errorf("make channel send: %w", err) + } + + if _, err = c.GetResult(msgID); err != nil { + return 0, fmt.Errorf("make channel receive: %w", err) + } + + c.openedChannels[channel] = id + + return +} + +func (c *dtxMessageClient) RegisterCallback(obj string, cb func(m DTXMessageResult)) { + c.callbackMap[obj] = cb +} + +func (c *dtxMessageClient) GetResult(key interface{}) (*DTXMessageResult, error) { + startTime := time.Now() + for { + time.Sleep(100 * time.Millisecond) + c.mu.Lock() + if v, ok := c.resultMap[key]; ok { + delete(c.resultMap, key) + c.mu.Unlock() + return v, nil + } else { + c.mu.Unlock() + } + if elapsed := time.Since(startTime); elapsed > 30*time.Second { + return nil, fmt.Errorf("dtx: get result: timeout after %v", elapsed) + } + } +} + +func (c *dtxMessageClient) Close() { + c.cancelFunc() + c.innerConn.Close() +} + +func (c *dtxMessageClient) startReceive() { + go func() { + for { + select { + case <-c.ctx.Done(): + return + default: + if _, err := c.ReceiveDTXMessage(); err != nil { + debugLog(fmt.Sprintf("dtx: receive: %s", err)) + if strings.Contains(err.Error(), io.EOF.Error()) { + c.cancelFunc() + c.callbackMap[_over](DTXMessageResult{}) + break + } + } + } + } + }() +} + +func (c *dtxMessageClient) startWaitingForReply() { + go func() { + for { + select { + case <-c.ctx.Done(): + return + case reqHeader := <-c.toReply: + replyPayload := new(dtxMessagePayloadPacket) + replyPayload.Flags = 0 + replyPayload.AuxiliaryLength = 0 + replyPayload.TotalLength = 0 + + replyHeader := new(dtxMessageHeaderPacket) + replyHeader.Magic = 0x1F3D5B79 + replyHeader.CB = uint32(unsafe.Sizeof(*replyHeader)) + replyHeader.FragmentId = 0 + replyHeader.FragmentCount = 1 + replyHeader.Length = uint32(unsafe.Sizeof(*replyPayload)) + uint32(replyPayload.TotalLength) + replyHeader.Identifier = reqHeader.Identifier + replyHeader.ConversationIndex = reqHeader.ConversationIndex + 1 + replyHeader.ChannelCode = reqHeader.ChannelCode + replyHeader.ExpectsReply = 0 + + replyPkt := new(dtxMessagePacket) + replyPkt.Header = replyHeader + replyPkt.Payload = replyPayload + replyPkt.Aux = nil + replyPkt.Sel = nil + + raw, err := replyPkt.Pack() + if err != nil { + debugLog(fmt.Sprintf("pack: reply DTXMessage: %s", err)) + continue + } + + if err = c.innerConn.Write(raw); err != nil { + debugLog(fmt.Sprintf("send: reply DTXMessage: %s", err)) + continue + } + } + } + }() +} + +type DTXMessageResult struct { + Obj interface{} + Aux []interface{} +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/client_servicepacket.go b/hrp/pkg/gidevice/pkg/libimobiledevice/client_servicepacket.go new file mode 100644 index 00000000..6d071279 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/client_servicepacket.go @@ -0,0 +1,81 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "fmt" + + "howett.net/plist" +) + +func newServicePacketClient(innerConn InnerConn) *servicePacketClient { + return &servicePacketClient{ + innerConn: innerConn, + } +} + +type servicePacketClient struct { + innerConn InnerConn +} + +func (c *servicePacketClient) NewXmlPacket(req interface{}) (Packet, error) { + return c.newPacket(req, plist.XMLFormat) +} + +func (c *servicePacketClient) NewBinaryPacket(req interface{}) (Packet, error) { + return c.newPacket(req, plist.BinaryFormat) +} + +func (c *servicePacketClient) newPacket(req interface{}, format int) (Packet, error) { + pkt := new(servicePacket) + if buf, err := plist.Marshal(req, format); err != nil { + return nil, fmt.Errorf("plist packet marshal: %w", err) + } else { + pkt.body = buf + } + pkt.length = uint32(len(pkt.body)) + return pkt, nil +} + +func (c *servicePacketClient) SendPacket(pkt Packet) (err error) { + var raw []byte + if raw, err = pkt.Pack(); err != nil { + return fmt.Errorf("send packet: %w", err) + } + debugLog(fmt.Sprintf("--> %s\n", pkt)) + return c.innerConn.Write(raw) +} + +func (c *servicePacketClient) ReceivePacket() (respPkt Packet, err error) { + var bufLen []byte + if bufLen, err = c.innerConn.Read(4); err != nil { + return nil, fmt.Errorf("receive packet: %w", err) + } + lenPkg := binary.BigEndian.Uint32(bufLen) + + buffer := bytes.NewBuffer([]byte{}) + buffer.Write(bufLen) + + var buf []byte + if buf, err = c.innerConn.Read(int(lenPkg)); err != nil { + return nil, fmt.Errorf("receive packet: %w", err) + } + buffer.Write(buf) + + if respPkt, err = new(servicePacket).Unpack(buffer); err != nil { + return nil, fmt.Errorf("receive packet: %w", err) + } + + debugLog(fmt.Sprintf("<-- %s\n", respPkt)) + + var reply LockdownBasicResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, fmt.Errorf("receive packet: %w", err) + } + + if reply.Error != "" { + return nil, fmt.Errorf("receive packet: %s", reply.Error) + } + + return +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/crashreportmover.go b/hrp/pkg/gidevice/pkg/libimobiledevice/crashreportmover.go new file mode 100644 index 00000000..4d4ca85b --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/crashreportmover.go @@ -0,0 +1,20 @@ +package libimobiledevice + +const ( + CrashReportMoverServiceName = "com.apple.crashreportmover" + CrashReportCopyMobileServiceName = "com.apple.crashreportcopymobile" +) + +func NewCrashReportMoverClient(innerConn InnerConn) *CrashReportMoverClient { + return &CrashReportMoverClient{ + newServicePacketClient(innerConn), + } +} + +type CrashReportMoverClient struct { + client *servicePacketClient +} + +func (c *CrashReportMoverClient) InnerConn() InnerConn { + return c.client.innerConn +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/diagnosticsrelay.go b/hrp/pkg/gidevice/pkg/libimobiledevice/diagnosticsrelay.go new file mode 100644 index 00000000..f38b416a --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/diagnosticsrelay.go @@ -0,0 +1,39 @@ +package libimobiledevice + +const ( + DiagnosticsRelayServiceName = "com.apple.mobile.diagnostics_relay" +) + +type DiagnosticsRelayBasicRequest struct { + Request string `plist:"Request"` + Label string `plist:"Label"` +} + +func NewDiagnosticsRelayClient(innerConn InnerConn) *DiagnosticsRelayClient { + return &DiagnosticsRelayClient{ + newServicePacketClient(innerConn), + } +} + +type DiagnosticsRelayClient struct { + client *servicePacketClient +} + +func (c *DiagnosticsRelayClient) InnerConn() InnerConn { + return c.client.innerConn +} + +func (c *DiagnosticsRelayClient) NewBasicRequest(relayType string) *DiagnosticsRelayBasicRequest { + return &DiagnosticsRelayBasicRequest{ + Request: relayType, + Label: BundleID, + } +} + +func (c *DiagnosticsRelayClient) NewXmlPacket(req interface{}) (Packet, error) { + return c.client.NewXmlPacket(req) +} + +func (c *DiagnosticsRelayClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/housearrest.go b/hrp/pkg/gidevice/pkg/libimobiledevice/housearrest.go new file mode 100644 index 00000000..4d219a7c --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/housearrest.go @@ -0,0 +1,56 @@ +package libimobiledevice + +const HouseArrestServiceName = "com.apple.mobile.house_arrest" + +const ( + CommandTypeVendDocuments CommandType = "VendDocuments" + CommandTypeVendContainer CommandType = "VendContainer" +) + +func NewHouseArrestClient(innerConn InnerConn) *HouseArrestClient { + return &HouseArrestClient{ + newServicePacketClient(innerConn), + } +} + +type HouseArrestClient struct { + client *servicePacketClient +} + +func (c *HouseArrestClient) NewBasicRequest(cmdType CommandType, bundleID string) *HouseArrestBasicRequest { + return &HouseArrestBasicRequest{ + Command: cmdType, + Identifier: bundleID, + } +} + +func (c *HouseArrestClient) NewDocumentsRequest(bundleID string) *HouseArrestBasicRequest { + return c.NewBasicRequest(CommandTypeVendDocuments, bundleID) +} + +func (c *HouseArrestClient) NewContainerRequest(bundleID string) *HouseArrestBasicRequest { + return c.NewBasicRequest(CommandTypeVendContainer, bundleID) +} + +func (c *HouseArrestClient) NewXmlPacket(req interface{}) (Packet, error) { + return c.client.NewXmlPacket(req) +} + +func (c *HouseArrestClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +func (c *HouseArrestClient) ReceivePacket() (respPkt Packet, err error) { + return c.client.ReceivePacket() +} + +func (c *HouseArrestClient) InnerConn() InnerConn { + return c.client.innerConn +} + +type ( + HouseArrestBasicRequest struct { + Command CommandType `plist:"Command"` + Identifier string `plist:"Identifier"` + } +) diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/imagemounter.go b/hrp/pkg/gidevice/pkg/libimobiledevice/imagemounter.go new file mode 100644 index 00000000..7dd7ba9d --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/imagemounter.go @@ -0,0 +1,107 @@ +package libimobiledevice + +import ( + "errors" + "fmt" + "io" + "strings" +) + +const ImageMounterServiceName = "com.apple.mobile.mobile_image_mounter" + +var ErrDeviceLocked = errors.New("device locked") + +type CommandType string + +const ( + CommandTypeLookupImage CommandType = "LookupImage" + CommandTypeReceiveBytes CommandType = "ReceiveBytes" + CommandTypeMountImage CommandType = "MountImage" +) + +func NewImageMounterClient(innerConn InnerConn) *ImageMounterClient { + return &ImageMounterClient{ + client: newServicePacketClient(innerConn), + } +} + +type ImageMounterClient struct { + client *servicePacketClient +} + +func (c *ImageMounterClient) NewBasicRequest(cmdType CommandType, imgType string) *ImageMounterBasicRequest { + return &ImageMounterBasicRequest{ + Command: cmdType, + ImageType: imgType, + } +} + +func (c *ImageMounterClient) NewReceiveBytesRequest(imgType string, imgSize uint32, imgSignature []byte) *ImageMounterReceiveBytesRequest { + return &ImageMounterReceiveBytesRequest{ + ImageMounterBasicRequest: *c.NewBasicRequest(CommandTypeReceiveBytes, imgType), + ImageSize: imgSize, + ImageSignature: imgSignature, + } +} + +func (c *ImageMounterClient) NewMountImageRequest(imgType, imgPath string, imgSignature []byte) *ImageMounterMountImageRequest { + return &ImageMounterMountImageRequest{ + ImageMounterBasicRequest: *c.NewBasicRequest(CommandTypeMountImage, imgType), + ImagePath: imgPath, + ImageSignature: imgSignature, + } +} + +func (c *ImageMounterClient) NewXmlPacket(req interface{}) (Packet, error) { + return c.client.NewXmlPacket(req) +} + +func (c *ImageMounterClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +func (c *ImageMounterClient) ReceivePacket() (respPkt Packet, err error) { + respPkt, err = c.client.ReceivePacket() + if err != nil { + if strings.Contains(err.Error(), io.EOF.Error()) { + return nil, ErrDeviceLocked + } + } + return +} + +func (c *ImageMounterClient) SendDmg(data []byte) (err error) { + debugLog(fmt.Sprintf("--> ...DmgData...\n")) + return c.client.innerConn.Write(data) +} + +type ( + ImageMounterBasicRequest struct { + Command CommandType `plist:"Command"` + ImageType string `plist:"ImageType"` + } + + ImageMounterReceiveBytesRequest struct { + ImageMounterBasicRequest + ImageSignature []byte `plist:"ImageSignature"` + ImageSize uint32 `plist:"ImageSize"` + } + + ImageMounterMountImageRequest struct { + ImageMounterBasicRequest + ImagePath string `plist:"ImagePath"` + ImageSignature []byte `plist:"ImageSignature"` + } +) + +type ( + ImageMounterBasicResponse struct { + LockdownBasicResponse + Status string `plist:"Status"` + } + + ImageMounterLookupImageResponse struct { + ImageMounterBasicResponse + ImageSignature [][]byte `plist:"ImageSignature"` + } +) diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/installationproxy.go b/hrp/pkg/gidevice/pkg/libimobiledevice/installationproxy.go new file mode 100644 index 00000000..9500912e --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/installationproxy.go @@ -0,0 +1,119 @@ +package libimobiledevice + +const InstallationProxyServiceName = "com.apple.mobile.installation_proxy" + +const ( + CommandTypeBrowse CommandType = "Browse" + CommandTypeLookup CommandType = "Lookup" + CommandTypeInstall CommandType = "Install" + CommandTypeUninstall CommandType = "Uninstall" +) + +type ApplicationType string + +const ( + ApplicationTypeSystem ApplicationType = "System" + ApplicationTypeUser ApplicationType = "User" + ApplicationTypeInternal ApplicationType = "internal" + ApplicationTypeAny ApplicationType = "Any" +) + +func NewInstallationProxyClient(innerConn InnerConn) *InstallationProxyClient { + return &InstallationProxyClient{ + client: newServicePacketClient(innerConn), + } +} + +type InstallationProxyClient struct { + client *servicePacketClient +} + +func (c *InstallationProxyClient) NewBasicRequest(cmdType CommandType, opt *InstallationProxyOption) *InstallationProxyBasicRequest { + req := &InstallationProxyBasicRequest{Command: cmdType} + if opt != nil { + req.ClientOptions = opt + } + return req +} + +func (c *InstallationProxyClient) NewInstallRequest(bundleID, packagePath string) *InstallationProxyInstallRequest { + opt := &InstallationProxyOption{ + BundleID: bundleID, + } + req := &InstallationProxyInstallRequest{ + Command: CommandTypeInstall, + ClientOptions: opt, + PackagePath: packagePath, + } + return req +} + +func (c *InstallationProxyClient) NewUninstallRequest(bundleID string) *InstallationProxyUninstallRequest { + req := &InstallationProxyUninstallRequest{ + Command: CommandTypeUninstall, + BundleID: bundleID, + } + return req +} + +func (c *InstallationProxyClient) NewXmlPacket(req interface{}) (Packet, error) { + return c.client.NewXmlPacket(req) +} + +func (c *InstallationProxyClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +func (c *InstallationProxyClient) ReceivePacket() (respPkt Packet, err error) { + return c.client.ReceivePacket() +} + +type InstallationProxyOption struct { + ApplicationType ApplicationType `plist:"ApplicationType,omitempty"` + ReturnAttributes []string `plist:"ReturnAttributes,omitempty"` + MetaData bool `plist:"com.apple.mobile_installation.metadata,omitempty"` + BundleIDs []string `plist:"BundleIDs,omitempty"` // for Lookup + BundleID string `plist:"CFBundleIdentifier,omitempty"` // for Install +} + +type ( + InstallationProxyBasicRequest struct { + Command CommandType `plist:"Command"` + ClientOptions *InstallationProxyOption `plist:"ClientOptions,omitempty"` + } + + InstallationProxyInstallRequest struct { + Command CommandType `plist:"Command"` + ClientOptions *InstallationProxyOption `plist:"ClientOptions"` + PackagePath string `plist:"PackagePath"` + } + + InstallationProxyUninstallRequest struct { + Command CommandType `plist:"Command"` + BundleID string `plist:"ApplicationIdentifier"` + } +) + +type ( + InstallationProxyBasicResponse struct { + Status string `plist:"Status"` + } + + InstallationProxyLookupResponse struct { + InstallationProxyBasicResponse + LookupResult interface{} `plist:"LookupResult"` + } + + InstallationProxyBrowseResponse struct { + InstallationProxyBasicResponse + CurrentAmount int `plist:"CurrentAmount"` + CurrentIndex int `plist:"CurrentIndex"` + CurrentList []interface{} `plist:"CurrentList"` + } + + InstallationProxyInstallResponse struct { + InstallationProxyBasicResponse + Error string `plist:"Error"` + ErrorDescription string `plist:"ErrorDescription"` + } +) diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/instruments.go b/hrp/pkg/gidevice/pkg/libimobiledevice/instruments.go new file mode 100644 index 00000000..63e9a2a0 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/instruments.go @@ -0,0 +1,41 @@ +package libimobiledevice + +const ( + InstrumentsServiceName = "com.apple.instruments.remoteserver" + InstrumentsSecureProxyServiceName = "com.apple.instruments.remoteserver.DVTSecureSocketProxy" +) + +func NewInstrumentsClient(innerConn InnerConn) *InstrumentsClient { + return &InstrumentsClient{ + client: newDtxMessageClient(innerConn), + } +} + +type InstrumentsClient struct { + client *dtxMessageClient +} + +func (c *InstrumentsClient) NotifyOfPublishedCapabilities() (publishedChannels map[string]int32, err error) { + return c.client.Connection() +} + +func (c *InstrumentsClient) RequestChannel(channel string) (id uint32, err error) { + return c.client.MakeChannel(channel) +} + +func (c *InstrumentsClient) Invoke(selector string, args *AuxBuffer, channelCode uint32, expectsReply bool) (result *DTXMessageResult, err error) { + var msgID uint32 + if msgID, err = c.client.SendDTXMessage(selector, args.Bytes(), channelCode, expectsReply); err != nil { + return nil, err + } + if expectsReply { + if result, err = c.client.GetResult(msgID); err != nil { + return nil, err + } + } + return +} + +func (c *InstrumentsClient) RegisterCallback(obj string, cb func(m DTXMessageResult)) { + c.client.RegisterCallback(obj, cb) +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/keyedarchiver.go b/hrp/pkg/gidevice/pkg/libimobiledevice/keyedarchiver.go new file mode 100644 index 00000000..f942c5c6 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/keyedarchiver.go @@ -0,0 +1,260 @@ +package libimobiledevice + +import ( + "reflect" + "strconv" + "time" + + "howett.net/plist" +) + +const nsNull = "$null" + +func newKeyedArchiver() *KeyedArchiver { + return &KeyedArchiver{ + Archiver: "NSKeyedArchiver", + Version: 100000, + } +} + +type KeyedArchiver struct { + Archiver string `plist:"$archiver"` + Objects []interface{} `plist:"$objects"` + Top ArchiverRoot `plist:"$top"` + Version int `plist:"$version"` +} + +func (ka *KeyedArchiver) UID() plist.UID { + return plist.UID(len(ka.Objects)) +} + +type ArchiverRoot struct { + Root plist.UID `plist:"root"` +} + +type ArchiverClasses struct { + Classes []string `plist:"$classes"` + ClassName string `plist:"$classname"` +} + +var ( + NSMutableDictionaryClass = &ArchiverClasses{ + Classes: []string{"NSMutableDictionary", "NSDictionary", "NSObject"}, + ClassName: "NSMutableDictionary", + } + NSDictionaryClass = &ArchiverClasses{ + Classes: []string{"NSDictionary", "NSObject"}, + ClassName: "NSDictionary", + } + NSMutableArrayClass = &ArchiverClasses{ + Classes: []string{"NSMutableArray", "NSArray", "NSObject"}, + ClassName: "NSMutableArray", + } + NSArrayClass = &ArchiverClasses{ + Classes: []string{"NSArray", "NSObject"}, + ClassName: "NSArray", + } + NSMutableDataClass = &ArchiverClasses{ + Classes: []string{"NSMutableArray", "NSArray", "NSObject"}, + ClassName: "NSMutableArray", + } + NSDataClass = &ArchiverClasses{ + Classes: []string{"NSData", "NSObject"}, + ClassName: "NSData", + } + NSDateClass = &ArchiverClasses{ + Classes: []string{"NSDate", "NSObject"}, + ClassName: "NSDate", + } + NSErrorClass = &ArchiverClasses{ + Classes: []string{"NSError", "NSObject"}, + ClassName: "NSError", + } +) + +type NSObject struct { + Class plist.UID `plist:"$class"` +} + +type NSArray struct { + NSObject + Values []plist.UID `plist:"NS.objects"` +} + +type NSDictionary struct { + NSArray + Keys []plist.UID `plist:"NS.keys"` +} + +type NSData struct { + NSObject + Data []byte `plist:"NS.data"` +} + +type NSError struct { + NSCode int + NSDomain string + NSUserInfo interface{} +} + +type NSKeyedArchiver struct { + objRefVal []interface{} + objRef map[interface{}]plist.UID +} + +func NewNSKeyedArchiver() *NSKeyedArchiver { + return &NSKeyedArchiver{ + objRef: make(map[interface{}]plist.UID), + } +} + +func (ka *NSKeyedArchiver) id(v interface{}) plist.UID { + var ref plist.UID + if id, ok := ka.objRef[v]; !ok { + ref = plist.UID(len(ka.objRef)) + ka.objRefVal = append(ka.objRefVal, v) + ka.objRef[v] = ref + } else { + ref = id + } + return ref +} + +func (ka *NSKeyedArchiver) flushToStruct(root *KeyedArchiver) { + for i := 0; i < len(ka.objRefVal); i++ { + val := ka.objRefVal[i] + vt := reflect.ValueOf(val) + if vt.Kind() == reflect.Ptr { + val = vt.Elem().Interface() + } + root.Objects = append(root.Objects, val) + } +} + +func (ka *NSKeyedArchiver) clear() { + ka.objRef = make(map[interface{}]plist.UID) + ka.objRefVal = []interface{}{} +} + +type XCTestConfiguration struct { + Contents map[string]interface{} +} + +func (ka *NSKeyedArchiver) Marshal(obj interface{}) ([]byte, error) { + val := reflect.ValueOf(obj) + typ := val.Type() + + root := newKeyedArchiver() + + var tmpTop plist.UID + + ka.id(nsNull) + + switch typ.Kind() { + case reflect.Map: + m := &NSDictionary{} + m.Class = ka.id(NSDictionaryClass) + keys := val.MapKeys() + for _, v := range keys { + m.Keys = append(m.Keys, ka.id(v.Interface())) + m.Values = append(m.Values, ka.id(val.MapIndex(v).Interface())) + } + tmpTop = ka.id(m) + case reflect.Slice, reflect.Array: + if typ.Elem().Kind() == reflect.Uint8 { + d := &NSData{} + d.Class = ka.id(NSDataClass) + var w []byte + for i := 0; i < val.Len(); i++ { + w = append(w, uint8(val.Index(i).Uint())) + } + d.Data = w + } + a := &NSArray{} + a.Class = ka.id(NSArrayClass) + for i := 0; i < val.Len(); i++ { + a.Values = append(a.Values, ka.id(val.Index(i).Interface())) + } + tmpTop = ka.id(a) + case reflect.String: + tmpTop = ka.id(obj) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + tmpTop = ka.id(obj) + } + + root.Top.Root = tmpTop + + ka.flushToStruct(root) + + ka.clear() + + return plist.Marshal(root, plist.BinaryFormat) +} + +func (ka *NSKeyedArchiver) convertValue(v interface{}) interface{} { + if m, ok := v.(map[string]interface{}); ok { + className := ka.objRefVal[m["$class"].(plist.UID)].(map[string]interface{})["$classname"] + + switch className { + case NSMutableDictionaryClass.Classes[0], NSDictionaryClass.Classes[0]: + ret := make(map[string]interface{}) + keys := m["NS.keys"].([]interface{}) + values := m["NS.objects"].([]interface{}) + + for i := 0; i < len(keys); i++ { + var keyValue string + key := ka.objRefVal[keys[i].(plist.UID)] + switch key.(type) { + case uint64: + keyValue = strconv.Itoa(int(key.(uint64))) + break + default: + keyValue = key.(string) + } + val := ka.convertValue(ka.objRefVal[values[i].(plist.UID)]) + ret[keyValue] = val + } + return ret + case NSMutableArrayClass.Classes[0], NSArrayClass.Classes[0]: + ret := make([]interface{}, 0) + values := m["NS.objects"].([]interface{}) + for i := 0; i < len(values); i++ { + ret = append(ret, ka.convertValue(values[i])) + } + return ret + case NSMutableDataClass.Classes[0], NSDataClass.Classes[0]: + return m["NS.data"].([]byte) + case NSDateClass.Classes[0]: + return time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC). + Add(time.Duration(m["NS.time"].(float64)) * time.Second) + case NSErrorClass.Classes[0]: + err := &NSError{} + err.NSCode = int(m["NSCode"].(uint64)) + err.NSDomain = ka.objRefVal[m["NSDomain"].(plist.UID)].(string) + err.NSUserInfo = ka.convertValue(ka.objRefVal[m["NSUserInfo"].(plist.UID)]) + return *err + } + } else if uid, ok := v.(plist.UID); ok { + return ka.convertValue(ka.objRefVal[uid]) + } + return v +} + +func (ka *NSKeyedArchiver) Unmarshal(b []byte) (interface{}, error) { + archiver := new(KeyedArchiver) + + if _, err := plist.Unmarshal(b, archiver); err != nil { + return nil, err + } + + for _, v := range archiver.Objects { + ka.objRefVal = append(ka.objRefVal, v) + } + + ret := ka.convertValue(ka.objRefVal[archiver.Top.Root]) + + ka.clear() + + return ret, nil +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/lib.go b/hrp/pkg/gidevice/pkg/libimobiledevice/lib.go new file mode 100644 index 00000000..39e8c939 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/lib.go @@ -0,0 +1,28 @@ +package libimobiledevice + +import ( + "bytes" + "log" +) + +type Packet interface { + Pack() ([]byte, error) + Unpack(buffer *bytes.Buffer) (Packet, error) + Unmarshal(v interface{}) error + + String() string +} + +var debugFlag = false + +// SetDebug sets debug mode +func SetDebug(debug bool) { + debugFlag = debug +} + +func debugLog(msg string) { + if !debugFlag { + return + } + log.Printf("[%s-debug] %s\n", ProgramName, msg) +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/lockdown.go b/hrp/pkg/gidevice/pkg/libimobiledevice/lockdown.go new file mode 100644 index 00000000..e96f6b91 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/lockdown.go @@ -0,0 +1,183 @@ +package libimobiledevice + +const ProtocolVersion = "2" + +const LockdownPort = 62078 + +type RequestType string + +const ( + RequestTypeQueryType RequestType = "QueryType" + RequestTypeSetValue RequestType = "SetValue" + RequestTypeGetValue RequestType = "GetValue" + RequestTypePair RequestType = "Pair" + RequestTypeEnterRecovery RequestType = "EnterRecovery" + RequestTypeStartSession RequestType = "StartSession" + RequestTypeStopSession RequestType = "StopSession" + RequestTypeStartService RequestType = "StartService" +) + +type LockdownType struct { + Type string `plist:"Type"` +} + +func NewLockdownClient(innerConn InnerConn) *LockdownClient { + return &LockdownClient{ + client: newServicePacketClient(innerConn), + } +} + +type LockdownClient struct { + client *servicePacketClient +} + +func (c *LockdownClient) NewBasicRequest(reqType RequestType) *LockdownBasicRequest { + return &LockdownBasicRequest{ + Label: BundleID, + ProtocolVersion: ProtocolVersion, + Request: reqType, + } +} + +func (c *LockdownClient) NewGetValueRequest(domain, key string) *LockdownValueRequest { + return &LockdownValueRequest{ + LockdownBasicRequest: *c.NewBasicRequest(RequestTypeGetValue), + Domain: domain, + Key: key, + } +} + +func (c *LockdownClient) NewSetValueRequest(domain, key string, value interface{}) *LockdownValueRequest { + return &LockdownValueRequest{ + LockdownBasicRequest: *c.NewBasicRequest(RequestTypeSetValue), + Domain: domain, + Key: key, + Value: value, + } +} + +func (c *LockdownClient) NewEnterRecoveryRequest() *LockdownBasicRequest { + return c.NewBasicRequest(RequestTypeEnterRecovery) +} + +func (c *LockdownClient) NewPairRequest(pairRecord *PairRecord) *LockdownPairRequest { + return &LockdownPairRequest{ + LockdownBasicRequest: *c.NewBasicRequest(RequestTypePair), + PairRecord: pairRecord, + PairingOptions: map[string]interface{}{ + "ExtendedPairingErrors": true, + }, + } +} + +func (c *LockdownClient) NewStartSessionRequest(buid, hostID string) *LockdownStartSessionRequest { + return &LockdownStartSessionRequest{ + LockdownBasicRequest: *c.NewBasicRequest(RequestTypeStartSession), + SystemBUID: buid, + HostID: hostID, + } +} + +func (c *LockdownClient) NewStopSessionRequest(sessionID string) *LockdownStopSessionRequest { + return &LockdownStopSessionRequest{ + LockdownBasicRequest: *c.NewBasicRequest(RequestTypeStopSession), + SessionID: sessionID, + } +} + +func (c *LockdownClient) NewStartServiceRequest(service string) *LockdownStartServiceRequest { + return &LockdownStartServiceRequest{ + LockdownBasicRequest: *c.NewBasicRequest(RequestTypeStartService), + Service: service, + } +} + +func (c *LockdownClient) NewXmlPacket(req interface{}) (Packet, error) { + return c.client.NewXmlPacket(req) +} + +func (c *LockdownClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +func (c *LockdownClient) ReceivePacket() (respPkt Packet, err error) { + return c.client.ReceivePacket() +} + +func (c *LockdownClient) EnableSSL(version []int, pairRecord *PairRecord) (err error) { + return c.client.innerConn.Handshake(version, pairRecord) +} + +type ( + LockdownBasicRequest struct { + Label string `plist:"Label"` + ProtocolVersion string `plist:"ProtocolVersion"` + Request RequestType `plist:"Request"` + } + + LockdownValueRequest struct { + LockdownBasicRequest + Domain string `plist:"Domain,omitempty"` + Key string `plist:"Key,omitempty"` + Value interface{} `plist:"Value,omitempty"` + } + + LockdownPairRequest struct { + LockdownBasicRequest + PairRecord *PairRecord `plist:"PairRecord"` + PairingOptions map[string]interface{} `plist:"PairingOptions"` + } + + LockdownStartSessionRequest struct { + LockdownBasicRequest + SystemBUID string `plist:"SystemBUID"` + HostID string `plist:"HostID"` + } + + LockdownStopSessionRequest struct { + LockdownBasicRequest + SessionID string `plist:"SessionID"` + } + + LockdownStartServiceRequest struct { + LockdownBasicRequest + Service string `plist:"Service"` + EscrowBag []byte `plist:"EscrowBag,omitempty"` + } +) + +type ( + LockdownBasicResponse struct { + Request string `plist:"Request"` + Error string `plist:"Error"` + } + + LockdownTypeResponse struct { + LockdownBasicResponse + Type string `plist:"Type"` + } + + LockdownValueResponse struct { + LockdownBasicResponse + Key string `plist:"Key"` + Value interface{} `plist:"Value"` + } + + LockdownPairResponse struct { + LockdownBasicResponse + EscrowBag []byte `plist:"EscrowBag"` + } + + LockdownStartSessionResponse struct { + LockdownBasicResponse + EnableSessionSSL bool `plist:"EnableSessionSSL"` + SessionID string `plist:"SessionID"` + } + + LockdownStartServiceResponse struct { + LockdownBasicResponse + EnableServiceSSL bool `plist:"EnableServiceSSL"` + Port int `plist:"Port"` + Service string `plist:"Service"` + } +) diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/packet_afc.go b/hrp/pkg/gidevice/pkg/libimobiledevice/packet_afc.go new file mode 100644 index 00000000..d6a73cf2 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/packet_afc.go @@ -0,0 +1,86 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" +) + +var afcHeader = []byte{0x43, 0x46, 0x41, 0x36, 0x4C, 0x50, 0x41, 0x41} + +var _ Packet = (*afcPacket)(nil) + +type afcPacket struct { + entireLen uint64 + thisLen uint64 + packetNum uint64 + operation uint64 +} + +func (p *afcPacket) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + buf.Write(afcHeader) + + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, p.entireLen) + buf.Write(b) + binary.LittleEndian.PutUint64(b, p.thisLen) + buf.Write(b) + binary.LittleEndian.PutUint64(b, p.packetNum) + buf.Write(b) + binary.LittleEndian.PutUint64(b, p.operation) + buf.Write(b) + + return buf.Bytes(), nil +} + +func (p *afcPacket) Unpack(buffer *bytes.Buffer) (Packet, error) { + return p.unpack(buffer) +} + +func (p *afcPacket) unpack(buffer *bytes.Buffer) (*afcPacket, error) { + magic := make([]byte, 8) + if _, err := buffer.Read(magic); err != nil { + return nil, fmt.Errorf("afc packet unpack: %w", err) + } + if bytes.Compare(magic, afcHeader) != 0 { + return nil, errors.New("afc packet unpack: header not match") + } + + respPkt := new(afcPacket) + if err := binary.Read(buffer, binary.LittleEndian, &respPkt.entireLen); err != nil { + return nil, fmt.Errorf("afc packet unpack: %w", err) + } + if err := binary.Read(buffer, binary.LittleEndian, &respPkt.thisLen); err != nil { + return nil, fmt.Errorf("afc packet unpack: %w", err) + } + if err := binary.Read(buffer, binary.LittleEndian, &respPkt.packetNum); err != nil { + return nil, fmt.Errorf("afc packet unpack: %w", err) + } + if err := binary.Read(buffer, binary.LittleEndian, &respPkt.operation); err != nil { + return nil, fmt.Errorf("afc packet unpack: %w", err) + } + return respPkt, nil +} + +func (p *afcPacket) Unmarshal(v interface{}) error { + // switch msg := v.(type) { + // case *AfcMessage: + // // msg.EntireLen = p.entireLen + // // msg.ThisLen = p.thisLen + // // msg.PacketNum = p.packetNum + // msg.Operation = p.operation + // default: + // return errors.New("the type of the method parameter must be '*AfcMessage'") + // } + // return nil + panic("never use (afcPacket)") +} + +func (p *afcPacket) String() string { + return fmt.Sprintf( + "EntireLen: %d, ThisLen: %d, PacketNum: %d, Operation: %X\n", + p.entireLen, p.thisLen, p.packetNum, p.operation, + ) +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/packet_dtxmessage.go b/hrp/pkg/gidevice/pkg/libimobiledevice/packet_dtxmessage.go new file mode 100644 index 00000000..7c625d3a --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/packet_dtxmessage.go @@ -0,0 +1,203 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" +) + +var ( + _ Packet = (*dtxMessagePayloadPacket)(nil) + _ Packet = (*dtxMessageHeaderPacket)(nil) + _ Packet = (*dtxMessagePacket)(nil) +) + +type dtxMessagePayloadPacket struct { + Flags uint32 + AuxiliaryLength uint32 + TotalLength uint64 +} + +func (p *dtxMessagePayloadPacket) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, p.Flags) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.AuxiliaryLength) + buf.Write(b) + + b = make([]byte, 8) + binary.LittleEndian.PutUint64(b, p.TotalLength) + buf.Write(b) + + return buf.Bytes(), nil +} + +func (p *dtxMessagePayloadPacket) Unpack(buffer *bytes.Buffer) (pkt Packet, err error) { + return p.unpack(buffer) +} + +func (p *dtxMessagePayloadPacket) unpack(buffer *bytes.Buffer) (pkt *dtxMessagePayloadPacket, err error) { + respPkt := new(dtxMessagePayloadPacket) + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.Flags); err != nil { + return nil, fmt.Errorf("packet (DTXMessagePayloadHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.AuxiliaryLength); err != nil { + return nil, fmt.Errorf("packet (DTXMessagePayloadHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.TotalLength); err != nil { + return nil, fmt.Errorf("packet (DTXMessagePayloadHeader) unpack: %w", err) + } + return respPkt, nil +} + +func (p *dtxMessagePayloadPacket) Unmarshal(v interface{}) error { + panic("never use (dtxMessagePayloadHeader)") +} + +func (p *dtxMessagePayloadPacket) String() string { + return fmt.Sprintf("DTXMessagePayloadHeader Flags: %d, AuxiliaryLength: %d, TotalLength: %d\n", + p.Flags, p.AuxiliaryLength, p.TotalLength, + ) +} + +type dtxMessageHeaderPacket struct { + Magic uint32 + CB uint32 + FragmentId uint16 + FragmentCount uint16 + Length uint32 + Identifier uint32 + ConversationIndex uint32 + ChannelCode uint32 + ExpectsReply uint32 +} + +func (p *dtxMessageHeaderPacket) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, p.Magic) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.CB) + buf.Write(b) + + b = make([]byte, 2) + binary.LittleEndian.PutUint16(b, p.FragmentId) + buf.Write(b) + binary.LittleEndian.PutUint16(b, p.FragmentCount) + buf.Write(b) + + b = make([]byte, 4) + binary.LittleEndian.PutUint32(b, p.Length) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.Identifier) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.ConversationIndex) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.ChannelCode) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.ExpectsReply) + buf.Write(b) + + return buf.Bytes(), nil +} + +func (p *dtxMessageHeaderPacket) Unpack(buffer *bytes.Buffer) (pkt Packet, err error) { + return p.unpack(buffer) +} + +func (p *dtxMessageHeaderPacket) unpack(buffer *bytes.Buffer) (pkt *dtxMessageHeaderPacket, err error) { + respPkt := new(dtxMessageHeaderPacket) + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.Magic); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.CB); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.FragmentId); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.FragmentCount); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.Length); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.Identifier); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.ConversationIndex); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.ChannelCode); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.ExpectsReply); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + return respPkt, nil +} + +func (p *dtxMessageHeaderPacket) Unmarshal(v interface{}) error { + panic("never use (DTXMessageHeader)") +} + +func (p *dtxMessageHeaderPacket) String() string { + return fmt.Sprintf("DTXMessageHeader Magic: %d, CB: %d, FragmentId: %d, FragmentCount: %d\n"+ + "Length: %d, Identifier: %d, ConversationIndex: %d, ChannelCode: %d, ExpectsReply: %d\n", + p.Magic, p.CB, p.FragmentId, p.FragmentCount, + p.Length, p.Identifier, p.ConversationIndex, p.ChannelCode, p.ExpectsReply, + ) +} + +type dtxMessagePacket struct { + Header *dtxMessageHeaderPacket + Payload *dtxMessagePayloadPacket + Aux []byte + Sel []byte +} + +func (p *dtxMessagePacket) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + + raw, err := p.Header.Pack() + if err != nil { + return nil, fmt.Errorf("packet (DTXMessagePacket) pack: %w", err) + } + buf.Write(raw) + + if raw, err = p.Payload.Pack(); err != nil { + return nil, fmt.Errorf("packet (DTXMessagePacket) pack: %w", err) + } + buf.Write(raw) + + if p.Aux != nil || len(p.Aux) != 0 { + buf.Write(p.Aux) + } + if p.Sel != nil || len(p.Sel) != 0 { + buf.Write(p.Sel) + } + + return buf.Bytes(), nil +} + +func (p *dtxMessagePacket) Unpack(buffer *bytes.Buffer) (Packet, error) { + panic("implement me") +} + +func (p *dtxMessagePacket) Unmarshal(v interface{}) error { + panic("never use (DTXMessagePacket)") +} + +func (p *dtxMessagePacket) String() string { + return fmt.Sprintf( + "DTXMessagePacket %s\n%s\n"+ + "%s\n%s\n", + p.Header.String(), p.Payload.String(), + // p.Aux, p.Sel, + hex.Dump(p.Aux), hex.Dump(p.Sel), + ) +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/packet_location.go b/hrp/pkg/gidevice/pkg/libimobiledevice/packet_location.go new file mode 100644 index 00000000..90858738 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/packet_location.go @@ -0,0 +1,53 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "fmt" + "strconv" +) + +var _ Packet = (*locationPacket)(nil) + +type locationPacket struct { + lon float64 + lat float64 +} + +func (l *locationPacket) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + + if err := binary.Write(buf, binary.BigEndian, uint32(0)); err != nil { + return nil, fmt.Errorf("packet (location) pack: %w", err) + } + + latS := []byte(strconv.FormatFloat(l.lat, 'E', -1, 64)) + if err := binary.Write(buf, binary.BigEndian, uint32(len(latS))); err != nil { + return nil, fmt.Errorf("packet (location) pack: %w", err) + } + if err := binary.Write(buf, binary.BigEndian, latS); err != nil { + return nil, fmt.Errorf("packet (location) pack: %w", err) + } + + lonS := []byte(strconv.FormatFloat(l.lon, 'E', -1, 64)) + if err := binary.Write(buf, binary.BigEndian, uint32(len(lonS))); err != nil { + return nil, fmt.Errorf("packet (location) pack: %w", err) + } + if err := binary.Write(buf, binary.BigEndian, lonS); err != nil { + return nil, fmt.Errorf("packet (location) pack: %w", err) + } + + return buf.Bytes(), nil +} + +func (l *locationPacket) Unpack(buffer *bytes.Buffer) (Packet, error) { + panic("never use (location)") +} + +func (l *locationPacket) Unmarshal(v interface{}) error { + panic("never use (location)") +} + +func (l *locationPacket) String() string { + return fmt.Sprintf("lon: %v, lat: %v\n", l.lon, l.lat) +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/packet_service.go b/hrp/pkg/gidevice/pkg/libimobiledevice/packet_service.go new file mode 100644 index 00000000..ce8e9b4f --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/packet_service.go @@ -0,0 +1,47 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "fmt" + + "howett.net/plist" +) + +var _ Packet = (*servicePacket)(nil) + +type servicePacket struct { + length uint32 + body []byte +} + +func (p *servicePacket) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, p.length) + buf.Write(b) + + buf.Write(p.body) + + return buf.Bytes(), nil +} + +func (p *servicePacket) Unpack(buffer *bytes.Buffer) (pkt Packet, err error) { + respPkt := new(servicePacket) + if err = binary.Read(buffer, binary.BigEndian, &respPkt.length); err != nil { + return nil, fmt.Errorf("packet (service) unpack: %w", err) + } + respPkt.body = buffer.Bytes() + + return respPkt, nil +} + +func (p *servicePacket) Unmarshal(v interface{}) (err error) { + _, err = plist.Unmarshal(p.body, v) + return +} + +func (p *servicePacket) String() string { + return fmt.Sprintf("Length: %d\n%s", p.length, p.body) +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/packet_usbmux.go b/hrp/pkg/gidevice/pkg/libimobiledevice/packet_usbmux.go new file mode 100644 index 00000000..9bd8714c --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/packet_usbmux.go @@ -0,0 +1,112 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "fmt" + + "howett.net/plist" +) + +var _ Packet = (*packet)(nil) + +type packet struct { + length uint32 + version ProtoVersion + msgType ProtoMessageType + tag uint32 + body []byte +} + +func (p *packet) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, p.length) + buf.Write(b) + binary.LittleEndian.PutUint32(b, uint32(p.version)) + buf.Write(b) + binary.LittleEndian.PutUint32(b, uint32(p.msgType)) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.tag) + buf.Write(b) + + buf.Write(p.body) + + return buf.Bytes(), nil +} + +func (p *packet) Unpack(buffer *bytes.Buffer) (pkt Packet, err error) { + respPkt := new(packet) + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.length); err != nil { + return nil, fmt.Errorf("packet unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.version); err != nil { + return nil, fmt.Errorf("packet unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.msgType); err != nil { + return nil, fmt.Errorf("packet unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.tag); err != nil { + return nil, fmt.Errorf("packet unpack: %w", err) + } + respPkt.body = buffer.Bytes() + return respPkt, nil +} + +func (p *packet) Unmarshal(v interface{}) (err error) { + _, err = plist.Unmarshal(p.body, v) + return +} + +func (p *packet) String() string { + return fmt.Sprintf( + "Length: %d, Version: %d, Type: %d, Tag: %d\n%s", + p.length, p.version, p.msgType, p.tag, p.body, + ) +} + +type ( + BasicRequest struct { + MessageType MessageType `plist:"MessageType"` + BundleID string `plist:"BundleID,omitempty"` + ProgramName string `plist:"ProgName,omitempty"` + ClientVersionString string `plist:"ClientVersionString"` + LibUSBMuxVersion uint `plist:"kLibUSBMuxVersion"` + } + + ConnectRequest struct { + BasicRequest + DeviceID int `plist:"DeviceID"` + PortNumber int `plist:"PortNumber"` + } + + ReadPairRecordRequest struct { + BasicRequest + PairRecordID string `plist:"PairRecordID"` + } + + SavePairRecordRequest struct { + BasicRequest + PairRecordID string `plist:"PairRecordID"` + PairRecordData []byte `plist:"PairRecordData"` + DeviceID int `plist:"DeviceID"` + } + + DeletePairRecordRequest struct { + BasicRequest + PairRecordID string `plist:"PairRecordID"` + } +) + +type PairRecord struct { + DeviceCertificate []byte `plist:"DeviceCertificate"` + EscrowBag []byte `plist:"EscrowBag,omitempty"` + HostCertificate []byte `plist:"HostCertificate"` + HostPrivateKey []byte `plist:"HostPrivateKey,omitempty"` + HostID string `plist:"HostID"` + RootCertificate []byte `plist:"RootCertificate"` + RootPrivateKey []byte `plist:"RootPrivateKey,omitempty"` + SystemBUID string `plist:"SystemBUID"` + WiFiMACAddress string `plist:"WiFiMACAddress,omitempty"` +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/pcapd.go b/hrp/pkg/gidevice/pkg/libimobiledevice/pcapd.go new file mode 100644 index 00000000..aba2896e --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/pcapd.go @@ -0,0 +1,119 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "fmt" + "io/ioutil" + "time" + + "github.com/lunixbochs/struc" +) + +const PcapdServiceName = "com.apple.pcapd" + +func NewPcapdClient(innerConn InnerConn) *PcapdClient { + return &PcapdClient{ + client: newServicePacketClient(innerConn), + } +} + +type PcapdClient struct { + filter func(*IOSPacketHeader) bool + client *servicePacketClient +} + +func (c *PcapdClient) ReceivePacket() (respPkt Packet, err error) { + var bufLen []byte + if bufLen, err = c.client.innerConn.Read(4); err != nil { + return nil, fmt.Errorf("lockdown(Pcapd) receive: %w", err) + } + lenPkg := binary.BigEndian.Uint32(bufLen) + + buffer := bytes.NewBuffer([]byte{}) + buffer.Write(bufLen) + + var buf []byte + if buf, err = c.client.innerConn.Read(int(lenPkg)); err != nil { + return nil, fmt.Errorf("lockdown(Pcapd) receive: %w", err) + } + buffer.Write(buf) + + if respPkt, err = new(servicePacket).Unpack(buffer); err != nil { + return nil, fmt.Errorf("lockdown(Pcapd) receive: %w", err) + } + + debugLog(fmt.Sprintf("<-- %s\n", respPkt)) + + return +} + +type IOSPacketHeader struct { + HdrSize uint32 `struc:"uint32,big"` + Version uint8 `struc:"uint8,big"` + PacketSize uint32 `struc:"uint32,big"` + Type uint8 `struc:"uint8,big"` + Unit uint16 `struc:"uint16,big"` + IO uint8 `struc:"uint8,big"` + ProtocolFamily uint32 `struc:"uint32,big"` + FramePreLength uint32 `struc:"uint32,big"` + FramePstLength uint32 `struc:"uint32,big"` + IFName string `struc:"[16]byte"` + Pid int32 `struc:"int32,little"` + ProcName string `struc:"[17]byte"` + Unknown uint32 `struc:"uint32,little"` + Pid2 int32 `struc:"int32,little"` + ProcName2 string `struc:"[17]byte"` + Unknown2 [8]byte `struc:"[8]byte"` +} + +func (c *PcapdClient) GetPacket(buf []byte) ([]byte, error) { + iph := IOSPacketHeader{} + preader := bytes.NewReader(buf) + _ = struc.Unpack(preader, &iph) + + if c.filter != nil { + if !c.filter(&iph) { + return nil, nil + } + } + + packet, err := ioutil.ReadAll(preader) + if err != nil { + return packet, err + } + if iph.FramePreLength == 0 { + ext := []byte{0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0x08, 0x00} + return append(ext, packet...), nil + } + return packet, nil +} + +type PcaprecHdrS struct { + TsSec int `struc:"uint32,little"` /* timestamp seconds */ + TsUsec int `struc:"uint32,little"` /* timestamp microseconds */ + InclLen int `struc:"uint32,little"` /* number of octets of packet saved in file */ + OrigLen int `struc:"uint32,little"` /* actual length of packet */ +} + +func (c *PcapdClient) CreatePacket(packet []byte) ([]byte, error) { + now := time.Now() + phs := &PcaprecHdrS{ + int(now.Unix()), + int(now.UnixNano()/1e3 - now.Unix()*1e6), + len(packet), + len(packet), + } + var buf bytes.Buffer + err := struc.Pack(&buf, phs) + if err != nil { + return nil, err + } + + buf.Write(packet) + return buf.Bytes(), nil +} + +func (c *PcapdClient) Close() { + c.client.innerConn.Close() +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/screenshot.go b/hrp/pkg/gidevice/pkg/libimobiledevice/screenshot.go new file mode 100644 index 00000000..96d4b33c --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/screenshot.go @@ -0,0 +1,52 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +const ScreenshotServiceName = "com.apple.mobile.screenshotr" + +func NewScreenshotClient(innerConn InnerConn) *ScreenshotClient { + return &ScreenshotClient{ + client: newServicePacketClient(innerConn), + } +} + +type ScreenshotClient struct { + client *servicePacketClient +} + +func (c *ScreenshotClient) NewBinaryPacket(req interface{}) (Packet, error) { + return c.client.NewBinaryPacket(req) +} + +func (c *ScreenshotClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +func (c *ScreenshotClient) ReceivePacket() (respPkt Packet, err error) { + var bufLen []byte + if bufLen, err = c.client.innerConn.Read(4); err != nil { + return nil, fmt.Errorf("lockdown(Screenshot) receive: %w", err) + } + lenPkg := binary.BigEndian.Uint32(bufLen) + + buffer := bytes.NewBuffer([]byte{}) + buffer.Write(bufLen) + + var buf []byte + if buf, err = c.client.innerConn.Read(int(lenPkg)); err != nil { + return nil, fmt.Errorf("lockdown(Screenshot) receive: %w", err) + } + buffer.Write(buf) + + if respPkt, err = new(servicePacket).Unpack(buffer); err != nil { + return nil, fmt.Errorf("lockdown(Screenshot) receive: %w", err) + } + + debugLog(fmt.Sprintf("<-- %s\n", respPkt)) + + return +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/simulatelocation.go b/hrp/pkg/gidevice/pkg/libimobiledevice/simulatelocation.go new file mode 100644 index 00000000..008e96e6 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/simulatelocation.go @@ -0,0 +1,128 @@ +package libimobiledevice + +import ( + "fmt" + "math" + "strings" +) + +const SimulateLocationServiceName = "com.apple.dt.simulatelocation" + +type CoordinateSystem string + +const ( + CoordinateSystemWGS84 CoordinateSystem = "WGS84" + CoordinateSystemBD09 CoordinateSystem = "BD09" + CoordinateSystemGCJ02 CoordinateSystem = "GCJ02" +) + +func NewSimulateLocationClient(innerConn InnerConn) *SimulateLocationClient { + return &SimulateLocationClient{ + client: newServicePacketClient(innerConn), + } +} + +type SimulateLocationClient struct { + client *servicePacketClient +} + +func (c *SimulateLocationClient) NewLocationPacket(lon, lat float64, coordinateSystem CoordinateSystem) Packet { + switch CoordinateSystem(strings.ToUpper(string(coordinateSystem))) { + case CoordinateSystemGCJ02: + lon, lat = gcj02ToWGS84(lon, lat) + case CoordinateSystemBD09: + lon, lat = bd09ToWGS84(lon, lat) + case CoordinateSystemWGS84: + _, _ = lon, lat + default: + _, _ = lon, lat + } + + pkt := new(locationPacket) + pkt.lon = lon + pkt.lat = lat + return pkt +} + +func (c *SimulateLocationClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +// Recover try to revert back +func (c *SimulateLocationClient) Recover() error { + data := []byte{0x00, 0x00, 0x00, 0x01} + debugLog(fmt.Sprintf("--> %+v\n", data)) + return c.client.innerConn.Write(data) +} + +const ( + xPi = math.Pi * 3000.0 / 180.0 + offset = 0.00669342162296594323 + axis = 6378245.0 +) + +func isOutOfChina(lon, lat float64) bool { + return !(lon > 73.66 && lon < 135.05 && lat > 3.86 && lat < 53.55) +} + +func delta(lon, lat float64) (float64, float64) { + dLat := transformLat(lon-105.0, lat-35.0) + dLon := transformLng(lon-105.0, lat-35.0) + + radLat := lat / 180.0 * math.Pi + magic := math.Sin(radLat) + magic = 1 - offset*magic*magic + sqrtMagic := math.Sqrt(magic) + + dLat = (dLat * 180.0) / ((axis * (1 - offset)) / (magic * sqrtMagic) * math.Pi) + dLon = (dLon * 180.0) / (axis / sqrtMagic * math.Cos(radLat) * math.Pi) + + mgLat := lat + dLat + mgLon := lon + dLon + + return mgLon, mgLat +} + +func transformLat(lon, lat float64) float64 { + ret := -100.0 + 2.0*lon + 3.0*lat + 0.2*lat*lat + 0.1*lon*lat + 0.2*math.Sqrt(math.Abs(lon)) + ret += (20.0*math.Sin(6.0*lon*math.Pi) + 20.0*math.Sin(2.0*lon*math.Pi)) * 2.0 / 3.0 + ret += (20.0*math.Sin(lat*math.Pi) + 40.0*math.Sin(lat/3.0*math.Pi)) * 2.0 / 3.0 + ret += (160.0*math.Sin(lat/12.0*math.Pi) + 320*math.Sin(lat*math.Pi/30.0)) * 2.0 / 3.0 + return ret +} + +func transformLng(lon, lat float64) float64 { + ret := 300.0 + lon + 2.0*lat + 0.1*lon*lon + 0.1*lon*lat + 0.1*math.Sqrt(math.Abs(lon)) + ret += (20.0*math.Sin(6.0*lon*math.Pi) + 20.0*math.Sin(2.0*lon*math.Pi)) * 2.0 / 3.0 + ret += (20.0*math.Sin(lon*math.Pi) + 40.0*math.Sin(lon/3.0*math.Pi)) * 2.0 / 3.0 + ret += (150.0*math.Sin(lon/12.0*math.Pi) + 300.0*math.Sin(lon/30.0*math.Pi)) * 2.0 / 3.0 + return ret +} + +func gcj02ToWGS84(lon, lat float64) (float64, float64) { + if isOutOfChina(lon, lat) { + return lon, lat + } + + mgLon, mgLat := delta(lon, lat) + + return lon*2 - mgLon, lat*2 - mgLat +} + +func bd09ToGCJ02(lon, lat float64) (float64, float64) { + x := lon - 0.0065 + y := lat - 0.006 + + z := math.Sqrt(x*x+y*y) - 0.00002*math.Sin(y*xPi) + theta := math.Atan2(y, x) - 0.000003*math.Cos(x*xPi) + + gLon := z * math.Cos(theta) + gLat := z * math.Sin(theta) + + return gLon, gLat +} + +func bd09ToWGS84(lon, lat float64) (float64, float64) { + lon, lat = bd09ToGCJ02(lon, lat) + return gcj02ToWGS84(lon, lat) +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/springboard.go b/hrp/pkg/gidevice/pkg/libimobiledevice/springboard.go new file mode 100644 index 00000000..fa274131 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/springboard.go @@ -0,0 +1,53 @@ +package libimobiledevice + +type IconPNGDataResponse struct { + PNGData []byte `plist:"pngData"` +} + +type InterfaceOrientationResponse struct { + Orientation OrientationState `plist:"interfaceOrientation"` +} + +type OrientationState int64 + +const ( + Unknown OrientationState = iota + Portrait + PortraitUpsideDown + LandscapeRight + LandscapeLeft +) + +const ( + SpringBoardServiceName = "com.apple.springboardservices" +) + +func NewSpringBoardClient(innerConn InnerConn) *SpringBoardClient { + return &SpringBoardClient{ + newServicePacketClient(innerConn), + } +} + +type SpringBoardClient struct { + client *servicePacketClient +} + +func (c *SpringBoardClient) InnerConn() InnerConn { + return c.client.innerConn +} + +func (c *SpringBoardClient) NewXmlPacket(req interface{}) (Packet, error) { + return c.client.NewXmlPacket(req) +} + +func (c *SpringBoardClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +func (c *SpringBoardClient) ReceivePacket() (respPkt Packet, err error) { + return c.client.ReceivePacket() +} + +func (c *SpringBoardClient) NewBinaryPacket(req interface{}) (Packet, error) { + return c.client.NewBinaryPacket(req) +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/syslogrelay.go b/hrp/pkg/gidevice/pkg/libimobiledevice/syslogrelay.go new file mode 100644 index 00000000..2901f2de --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/syslogrelay.go @@ -0,0 +1,21 @@ +package libimobiledevice + +const SyslogRelayServiceName = "com.apple.syslog_relay" + +func NewSyslogRelayClient(innerConn InnerConn) *SyslogRelayClient { + return &SyslogRelayClient{ + newServicePacketClient(innerConn), + } +} + +type SyslogRelayClient struct { + client *servicePacketClient +} + +func (c *SyslogRelayClient) InnerConn() InnerConn { + return c.client.innerConn +} + +func (c *SyslogRelayClient) Close() { + c.client.innerConn.Close() +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/testmanagerd.go b/hrp/pkg/gidevice/pkg/libimobiledevice/testmanagerd.go new file mode 100644 index 00000000..64c88c26 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/testmanagerd.go @@ -0,0 +1,45 @@ +package libimobiledevice + +const ( + TestmanagerdSecureServiceName = "com.apple.testmanagerd.lockdown.secure" + TestmanagerdServiceName = "com.apple.testmanagerd.lockdown" +) + +func NewTestmanagerdClient(innerConn InnerConn) *TestmanagerdClient { + return &TestmanagerdClient{ + client: newDtxMessageClient(innerConn), + } +} + +type TestmanagerdClient struct { + client *dtxMessageClient +} + +func (t *TestmanagerdClient) Connection() (publishedChannels map[string]int32, err error) { + return t.client.Connection() +} + +func (t *TestmanagerdClient) MakeChannel(channel string) (id uint32, err error) { + return t.client.MakeChannel(channel) +} + +func (t *TestmanagerdClient) Invoke(selector string, args *AuxBuffer, channelCode uint32, expectsReply bool) (result *DTXMessageResult, err error) { + var msgID uint32 + if msgID, err = t.client.SendDTXMessage(selector, args.Bytes(), channelCode, expectsReply); err != nil { + return nil, err + } + if expectsReply { + if result, err = t.client.GetResult(msgID); err != nil { + return nil, err + } + } + return +} + +func (t *TestmanagerdClient) RegisterCallback(obj string, cb func(m DTXMessageResult)) { + t.client.RegisterCallback(obj, cb) +} + +func (t *TestmanagerdClient) Close() { + t.client.Close() +} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/usbmux.go b/hrp/pkg/gidevice/pkg/libimobiledevice/usbmux.go new file mode 100644 index 00000000..6f85507d --- /dev/null +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/usbmux.go @@ -0,0 +1,414 @@ +package libimobiledevice + +import ( + "bytes" + "crypto/tls" + "encoding/binary" + "fmt" + "net" + "runtime" + "strconv" + "time" + + "howett.net/plist" +) + +var DefaultDeadlineTimeout = 30 * time.Second + +const ( + BundleID = "electricbubble.libimobiledevice" + ProgramName = "libimobiledevice" + ClientVersion = "libimobiledevice-beta" + LibUSBMuxVersion = 3 +) + +type ReplyCode uint64 + +const ( + ReplyCodeOK ReplyCode = iota + ReplyCodeBadCommand + ReplyCodeBadDevice + ReplyCodeConnectionRefused + _ // ignore `4` + _ // ignore `5` + ReplyCodeBadVersion +) + +func (rc ReplyCode) String() string { + switch rc { + case ReplyCodeOK: + return "ok" + case ReplyCodeBadCommand: + return "bad command" + case ReplyCodeBadDevice: + return "bad device" + case ReplyCodeConnectionRefused: + return "connection refused" + case ReplyCodeBadVersion: + return "bad version" + default: + return "unknown reply code: " + strconv.Itoa(int(rc)) + } +} + +type ProtoVersion uint32 + +// proto_version == 1 +// construct message plist +// else `0`? res == `RESULT_BADVERSION` +// binary packet + +const ( + ProtoVersionBinary ProtoVersion = iota + ProtoVersionPlist +) + +type ProtoMessageType uint32 + +const ( + _ ProtoMessageType = iota + ProtoMessageTypeResult + ProtoMessageTypeConnect + ProtoMessageTypeListen + ProtoMessageTypeDeviceAdd + ProtoMessageTypeDeviceRemove + ProtoMessageTypeDevicePaired + _ // `7` + ProtoMessageTypePlist +) + +type MessageType string + +const ( + MessageTypeResult MessageType = "Result" + MessageTypeConnect MessageType = "Connect" + MessageTypeListen MessageType = "Listen" + MessageTypeDeviceAdd MessageType = "Attached" + MessageTypeDeviceRemove MessageType = "Detached" + MessageTypeReadBUID MessageType = "ReadBUID" + MessageTypeReadPairRecord MessageType = "ReadPairRecord" + MessageTypeSavePairRecord MessageType = "SavePairRecord" + MessageTypeDeletePairRecord MessageType = "DeletePairRecord" + MessageTypeDeviceList MessageType = "ListDevices" +) + +type BaseDevice struct { + MessageType MessageType `plist:"MessageType"` + DeviceID int `plist:"DeviceID"` + Properties DeviceProperties `plist:"Properties"` +} + +type DeviceProperties struct { + DeviceID int `plist:"DeviceID"` + ConnectionType string `plist:"ConnectionType"` + ConnectionSpeed int `plist:"ConnectionSpeed"` + ProductID int `plist:"ProductID"` + LocationID int `plist:"LocationID"` + SerialNumber string `plist:"SerialNumber"` + UDID string `plist:"UDID"` + USBSerialNumber string `plist:"USBSerialNumber"` + + EscapedFullServiceName string `plist:"EscapedFullServiceName"` + InterfaceIndex int `plist:"InterfaceIndex"` + NetworkAddress []byte `plist:"NetworkAddress"` +} + +func NewUsbmuxClient(timeout ...time.Duration) (c *UsbmuxClient, err error) { + if len(timeout) == 0 { + timeout = []time.Duration{DefaultDeadlineTimeout} + } + c = &UsbmuxClient{version: ProtoVersionPlist} + var conn net.Conn + if conn, err = rawDial(timeout[0]); err != nil { + return nil, fmt.Errorf("usbmux connect: %w", err) + } + + c.innerConn = newInnerConn(conn, timeout[0]) + return +} + +type UsbmuxClient struct { + innerConn InnerConn + version ProtoVersion + tag uint32 +} + +func (c *UsbmuxClient) NewBasicRequest(msgType MessageType) *BasicRequest { + return &BasicRequest{ + MessageType: msgType, + BundleID: BundleID, + ProgramName: ProgramName, + ClientVersionString: ClientVersion, + LibUSBMuxVersion: LibUSBMuxVersion, + } +} + +func (c *UsbmuxClient) NewConnectRequest(deviceID, port int) *ConnectRequest { + return &ConnectRequest{ + BasicRequest: *c.NewBasicRequest(MessageTypeConnect), + DeviceID: deviceID, + PortNumber: ((port << 8) & 0xFF00) | (port >> 8), + } +} + +func (c *UsbmuxClient) NewReadPairRecordRequest(udid string) *ReadPairRecordRequest { + return &ReadPairRecordRequest{ + BasicRequest: *c.NewBasicRequest(MessageTypeReadPairRecord), + PairRecordID: udid, + } +} + +func (c *UsbmuxClient) NewSavePairRecordRequest(udid string, deviceID int, data []byte) *SavePairRecordRequest { + return &SavePairRecordRequest{ + BasicRequest: *c.NewBasicRequest(MessageTypeSavePairRecord), + PairRecordID: udid, + PairRecordData: data, + DeviceID: deviceID, + } +} + +func (c *UsbmuxClient) NewDeletePairRecordRequest(udid string) *DeletePairRecordRequest { + return &DeletePairRecordRequest{ + BasicRequest: *c.NewBasicRequest(MessageTypeDeletePairRecord), + PairRecordID: udid, + } +} + +func (c *UsbmuxClient) NewPacket(protoMsgType ProtoMessageType) Packet { + return c.newPacket(protoMsgType) +} + +func (c *UsbmuxClient) newPacket(protoMsgType ProtoMessageType) *packet { + c.tag++ + pkt := &packet{ + version: c.version, + msgType: protoMsgType, + tag: c.tag, + } + return pkt +} + +func (c *UsbmuxClient) NewPlistPacket(req interface{}) (Packet, error) { + pkt := c.newPacket(ProtoMessageTypePlist) + if buf, err := plist.Marshal(req, plist.XMLFormat); err != nil { + return nil, fmt.Errorf("plist packet marshal: %w", err) + } else { + pkt.body = buf + } + pkt.length = uint32(len(pkt.body) + 4*4) + return pkt, nil +} + +func (c *UsbmuxClient) SendPacket(pkt Packet) (err error) { + var raw []byte + if raw, err = pkt.Pack(); err != nil { + return fmt.Errorf("usbmux send: %w", err) + } + // debugLog(fmt.Sprintf("--> Length: %d, Version: %d, Type: %d, Tag: %d\n%s\n", pkt.Length(), pkt.Version(), pkt.Type(), pkt.Tag(), pkt.Body())) + debugLog(fmt.Sprintf("--> %s\n", pkt)) + return c.innerConn.Write(raw) +} + +func (c *UsbmuxClient) ReceivePacket() (respPkt Packet, err error) { + var bufLen []byte + if bufLen, err = c.innerConn.Read(4); err != nil { + return nil, fmt.Errorf("usbmux receive: %w", err) + } + lenPkg := binary.LittleEndian.Uint32(bufLen) + + buffer := bytes.NewBuffer([]byte{}) + buffer.Write(bufLen) + + var buf []byte + if buf, err = c.innerConn.Read(int(lenPkg - 4)); err != nil { + return nil, fmt.Errorf("usbmux receive: %w", err) + } + buffer.Write(buf) + + if respPkt, err = new(packet).Unpack(buffer); err != nil { + return nil, fmt.Errorf("usbmux receive: %w", err) + } + + // debugLog(fmt.Sprintf("<-- Length: %d, Version: %d, Type: %d, Tag: %d\n%s\n", respPkt.Length(), respPkt.Version(), respPkt.Type(), respPkt.Tag(), respPkt.Body())) + debugLog(fmt.Sprintf("<-- %s\n", respPkt)) + + reply := struct { + MessageType string `plist:"MessageType"` + Number ReplyCode `plist:"Number"` + }{} + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, fmt.Errorf("usbmux receive: %w", err) + } + + if reply.Number != ReplyCodeOK { + return nil, fmt.Errorf("usbmux receive: %s", reply.Number.String()) + } + + return +} + +func (c *UsbmuxClient) Close() { + c.innerConn.Close() +} + +func (c *UsbmuxClient) RawConn() net.Conn { + return c.innerConn.RawConn() +} + +func (c *UsbmuxClient) InnerConn() InnerConn { + return c.innerConn +} + +func rawDial(timeout time.Duration) (net.Conn, error) { + dialer := net.Dialer{ + Timeout: timeout, + } + + var network, address string + switch runtime.GOOS { + case "darwin", "android", "linux": + network, address = "unix", "/var/run/usbmuxd" + case "windows": + network, address = "tcp", "127.0.0.1:27015" + default: + return nil, fmt.Errorf("raw dial: unsupported system: %s", runtime.GOOS) + } + + return dialer.Dial(network, address) +} + +type InnerConn interface { + Write(data []byte) (err error) + Read(length int) (data []byte, err error) + Handshake(version []int, pairRecord *PairRecord) (err error) + DismissSSL() (err error) + Close() + RawConn() net.Conn + Timeout(time.Duration) +} + +func newInnerConn(conn net.Conn, timeout time.Duration) InnerConn { + return &safeConn{ + conn: conn, + timeout: timeout, + } +} + +type safeConn struct { + conn net.Conn + sslConn *tls.Conn + timeout time.Duration +} + +func (c *safeConn) Write(data []byte) (err error) { + conn := c.RawConn() + if c.timeout <= 0 { + err = conn.SetWriteDeadline(time.Time{}) + } else { + err = conn.SetWriteDeadline(time.Now().Add(c.timeout)) + } + if err != nil { + return err + } + + for totalSent := 0; totalSent < len(data); { + var sent int + if sent, err = conn.Write(data[totalSent:]); err != nil { + return err + } + if sent == 0 { + return err + } + totalSent += sent + } + return +} + +func (c *safeConn) Read(length int) (data []byte, err error) { + conn := c.RawConn() + if c.timeout <= 0 { + err = conn.SetReadDeadline(time.Time{}) + } else { + err = conn.SetReadDeadline(time.Now().Add(c.timeout)) + } + if err != nil { + return nil, err + } + + data = make([]byte, 0, length) + for len(data) < length { + buf := make([]byte, length-len(data)) + _n, _err := 0, error(nil) + if _n, _err = conn.Read(buf); _err != nil && _n == 0 { + return nil, _err + } + data = append(data, buf[:_n]...) + } + return +} + +func (c *safeConn) Handshake(version []int, pairRecord *PairRecord) (err error) { + minVersion := uint16(tls.VersionTLS11) + maxVersion := uint16(tls.VersionTLS11) + + if version[0] > 10 { + minVersion = tls.VersionTLS11 + maxVersion = tls.VersionTLS13 + } + + cert, err := tls.X509KeyPair(pairRecord.RootCertificate, pairRecord.RootPrivateKey) + if err != nil { + return err + } + + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: true, + MinVersion: minVersion, + MaxVersion: maxVersion, + } + + c.sslConn = tls.Client(c.conn, config) + + if err = c.sslConn.Handshake(); err != nil { + return err + } + + return +} + +func (c *safeConn) DismissSSL() (err error) { + if c.sslConn != nil { + // err = c.sslConn.CloseWrite() + // if err = c.sslConn.CloseWrite(); err != nil { + // return err + // } + c.sslConn = nil + } + return +} + +func (c *safeConn) Close() { + if c.sslConn != nil { + if err := c.sslConn.Close(); err != nil { + debugLog(fmt.Sprintf("close: %s", err)) + } + } + if c.conn != nil { + if err := c.conn.Close(); err != nil { + debugLog(fmt.Sprintf("close: %s", err)) + } + } +} + +// RawConn `sslConn` first +func (c *safeConn) RawConn() net.Conn { + if c.sslConn != nil { + return c.sslConn + } + return c.conn +} + +func (c *safeConn) Timeout(duration time.Duration) { + c.timeout = duration +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsarray.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsarray.go new file mode 100644 index 00000000..385dada2 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsarray.go @@ -0,0 +1,37 @@ +package nskeyedarchiver + +import "howett.net/plist" + +type NSArray struct { + internal []interface{} +} + +func NewNSArray(value []interface{}) *NSArray { + return &NSArray{ + internal: value, + } +} + +func (ns *NSArray) archive(objects []interface{}) []interface{} { + objs := make([]interface{}, 0, len(ns.internal)) + + info := map[string]interface{}{} + objects = append(objects, info) + + for _, v := range ns.internal { + var uid plist.UID + objects, uid = archive(objects, v) + objs = append(objs, uid) + } + + info["NS.objects"] = objs + info["$class"] = plist.UID(len(objects)) + + cls := map[string]interface{}{ + "$classname": "NSArray", + "$classes": []interface{}{"NSArray", "NSObject"}, + } + objects = append(objects, cls) + + return objects +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsarray_test.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsarray_test.go new file mode 100644 index 00000000..5274bfc6 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsarray_test.go @@ -0,0 +1,18 @@ +package nskeyedarchiver + +import ( + "fmt" + "testing" +) + +func TestNSArray_archive(t *testing.T) { + objs := make([]interface{}, 0, 1) + value := []interface{}{ + "a", 1, + "b", "2", + "c", false, + } + array := NewNSArray(value) + objects := array.archive(objs) + fmt.Println(objects) +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsdictionary.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsdictionary.go new file mode 100644 index 00000000..80d442e9 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsdictionary.go @@ -0,0 +1,44 @@ +package nskeyedarchiver + +import ( + "howett.net/plist" +) + +type NSDictionary struct { + internal map[string]interface{} +} + +func NewNSDictionary(value map[string]interface{}) *NSDictionary { + return &NSDictionary{ + internal: value, + } +} + +func (ns *NSDictionary) archive(objects []interface{}) []interface{} { + keys := make([]interface{}, 0, len(ns.internal)) + objs := make([]interface{}, 0, len(ns.internal)) + + info := map[string]interface{}{} + objects = append(objects, info) + + for k, v := range ns.internal { + uid := plist.UID(len(objects)) + keys = append(keys, uid) + objects = append(objects, k) + + objects, uid = archive(objects, v) + objs = append(objs, uid) + } + + info["NS.keys"] = keys + info["NS.objects"] = objs + info["$class"] = plist.UID(len(objects)) + + cls := map[string]interface{}{ + "$classname": "NSDictionary", + "$classes": []interface{}{"NSDictionary", "NSObject"}, + } + objects = append(objects, cls) + + return objects +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsdictionary_test.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsdictionary_test.go new file mode 100644 index 00000000..e3becc6b --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsdictionary_test.go @@ -0,0 +1,18 @@ +package nskeyedarchiver + +import ( + "fmt" + "testing" +) + +func TestNSDictionary_archive(t *testing.T) { + objs := make([]interface{}, 0, 1) + value := map[string]interface{}{ + "a": 1, + "b": "2", + "c": true, + } + dict := NewNSDictionary(value) + objects := dict.archive(objs) + fmt.Println(objects) +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/nskeyedarchiver.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nskeyedarchiver.go new file mode 100644 index 00000000..172ad723 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nskeyedarchiver.go @@ -0,0 +1,80 @@ +package nskeyedarchiver + +import ( + "reflect" + + "howett.net/plist" +) + +func Marshal(obj interface{}) (raw []byte, err error) { + objects := []interface{}{"$null"} + objects, _ = archive(objects, obj) + archiver := map[string]interface{}{ + "$version": 100000, + "$archiver": "NSKeyedArchiver", + "$top": map[string]interface{}{"root": plist.UID(1)}, + "$objects": objects, + } + // if len(format) == 0 { + // format = []int{plist.BinaryFormat} + // } + // return plist.Marshal(archiver, format[0]) + return plist.Marshal(archiver, plist.BinaryFormat) +} + +func archive(_objects []interface{}, _value interface{}) (objects []interface{}, uid plist.UID) { + val := reflect.ValueOf(_value) + typ := val.Type() + + switch typ.Kind() { + case reflect.String, + reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Uintptr: + uid = plist.UID(len(_objects)) + objects = append(_objects, _value) + return + case reflect.Map: + uid = plist.UID(len(_objects)) + vv := make(map[string]interface{}) + keys := val.MapKeys() + for _, k := range keys { + vv[k.String()] = val.MapIndex(k).Interface() + } + objects = NewNSDictionary(vv).archive(_objects) + return + case reflect.Slice, reflect.Array: + uid = plist.UID(len(_objects)) + vv := make([]interface{}, val.Len()) + for i := 0; i < val.Len(); i++ { + vv[i] = val.Index(i).Interface() + } + objects = NewNSArray(vv).archive(_objects) + return + case reflect.Struct, reflect.Ptr: + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } + + switch typ.Name() { + case "NSUUID": + uid = plist.UID(len(_objects)) + objects = NewNSUUID(val.Field(0).Bytes()).archive(_objects) + return + case "NSURL": + uid = plist.UID(len(_objects)) + objects = NewNSURL(val.Field(0).String()).archive(_objects) + return + case "XCTestConfiguration": + uid = plist.UID(len(_objects)) + objects = newXCTestConfiguration(_value).archive(_objects) + return + } + } + + return +} + +// TODO unarchive diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/nskeyedarchiver_test.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nskeyedarchiver_test.go new file mode 100644 index 00000000..fa6cf849 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nskeyedarchiver_test.go @@ -0,0 +1,39 @@ +package nskeyedarchiver + +import ( + "fmt" + "testing" + + uuid "github.com/satori/go.uuid" +) + +func TestMarshal(t *testing.T) { + // value := map[string]interface{}{ + // "a": 1, + // "b": "2", + // "c": true, + // } + + // value := []interface{}{ + // "a", 1, + // "b", "2", + // "c", false, + // } + + // value := NewNSUUID(uuid.NewV4().Bytes()) + + // value := NewNSURL("/tmp") + + value := NewXCTestConfiguration(NewNSUUID(uuid.NewV4().Bytes()), NewNSURL("/tmp"), "", "") + + raw, err := Marshal(value) + if err != nil { + t.Fatal(err) + } + + for _, v := range raw { + fmt.Printf("%x", v) + } + fmt.Println() + // fmt.Println(raw) +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsnull.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsnull.go new file mode 100644 index 00000000..e553ce8f --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsnull.go @@ -0,0 +1,27 @@ +package nskeyedarchiver + +import ( + "howett.net/plist" +) + +type NSNull struct{} + +func NewNSNull() *NSNull { + return &NSNull{} +} + +func (ns *NSNull) archive(objects []interface{}) []interface{} { + info := map[string]interface{}{} + + objects = append(objects, info) + + info["$class"] = plist.UID(len(objects)) + + cls := map[string]interface{}{ + "$classname": "NSNull", + "$classes": []interface{}{"NSNull", "NSObject"}, + } + objects = append(objects, cls) + + return objects +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsurl.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsurl.go new file mode 100644 index 00000000..dbf5e112 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsurl.go @@ -0,0 +1,38 @@ +package nskeyedarchiver + +import ( + "fmt" + + "howett.net/plist" +) + +type NSURL struct { + internal string +} + +func NewNSURL(path string) *NSURL { + return &NSURL{ + internal: path, + } +} + +func (ns *NSURL) archive(objects []interface{}) []interface{} { + info := map[string]interface{}{} + + objects = append(objects, info) + + uid := plist.UID(0) + info["NS.base"] = uid + objects, uid = archive(objects, fmt.Sprintf("file://%s", ns.internal)) + info["NS.relative"] = uid + + info["$class"] = plist.UID(len(objects)) + + cls := map[string]interface{}{ + "$classname": "NSURL", + "$classes": []interface{}{"NSURL", "NSObject"}, + } + objects = append(objects, cls) + + return objects +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsurl_test.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsurl_test.go new file mode 100644 index 00000000..413a202d --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsurl_test.go @@ -0,0 +1,13 @@ +package nskeyedarchiver + +import ( + "fmt" + "testing" +) + +func TestNSURL_archive(t *testing.T) { + objs := make([]interface{}, 0, 1) + nsurl := NewNSURL("/tmp") + objects := nsurl.archive(objs) + fmt.Println(objects) +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsuuid.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsuuid.go new file mode 100644 index 00000000..3a2a9883 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsuuid.go @@ -0,0 +1,51 @@ +package nskeyedarchiver + +import ( + "encoding/hex" + + "howett.net/plist" +) + +type NSUUID struct { + internal []byte +} + +func NewNSUUID(uuid []byte) *NSUUID { + return &NSUUID{ + internal: uuid, + } +} + +func (ns *NSUUID) archive(objects []interface{}) []interface{} { + info := map[string]interface{}{ + "NS.uuidbytes": ns.internal, + } + + objects = append(objects, info) + + info["$class"] = plist.UID(len(objects)) + + cls := map[string]interface{}{ + "$classname": "NSUUID", + "$classes": []interface{}{"NSUUID", "NSObject"}, + } + objects = append(objects, cls) + + return objects +} + +func (ns *NSUUID) String() string { + buf := make([]byte, 36) + + hex.Encode(buf[0:8], ns.internal[0:4]) + buf[8] = '-' + hex.Encode(buf[9:13], ns.internal[4:6]) + buf[13] = '-' + hex.Encode(buf[14:18], ns.internal[6:8]) + buf[18] = '-' + hex.Encode(buf[19:23], ns.internal[8:10]) + buf[23] = '-' + hex.Encode(buf[24:], ns.internal[10:]) + + return string(buf) +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsuuid_test.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsuuid_test.go new file mode 100644 index 00000000..2b223ca8 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/nsuuid_test.go @@ -0,0 +1,15 @@ +package nskeyedarchiver + +import ( + "fmt" + "testing" + + uuid "github.com/satori/go.uuid" +) + +func TestNSUUID_archive(t *testing.T) { + objs := make([]interface{}, 0, 1) + nsuuid := NewNSUUID(uuid.NewV4().Bytes()) + objects := nsuuid.archive(objs) + fmt.Println(objects) +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/xctcapabilities.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/xctcapabilities.go new file mode 100644 index 00000000..a301c254 --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/xctcapabilities.go @@ -0,0 +1,10 @@ +package nskeyedarchiver + +type XCTCapabilities struct { + internal map[string]interface{} +} + +func (caps *XCTCapabilities) archive(objects []interface{}) []interface{} { + // TODO caps + return nil +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/xctestconfiguration.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/xctestconfiguration.go new file mode 100644 index 00000000..383e3c3f --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/xctestconfiguration.go @@ -0,0 +1,89 @@ +package nskeyedarchiver + +import ( + "reflect" + + "howett.net/plist" +) + +type XCTestConfiguration struct { + internal map[string]interface{} +} + +func newXCTestConfiguration(cfg interface{}) *XCTestConfiguration { + return cfg.(*XCTestConfiguration) +} + +func NewXCTestConfiguration(nsuuid *NSUUID, nsurl *NSURL, targetBundleID, targetAppPath string) *XCTestConfiguration { + contents := map[string]interface{}{ + "aggregateStatisticsBeforeCrash": map[string]interface{}{ + "XCSuiteRecordsKey": map[string]interface{}{}, + }, + "automationFrameworkPath": "/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework", + "baselineFileRelativePath": nil, + "baselineFileURL": nil, + "defaultTestExecutionTimeAllowance": nil, + "disablePerformanceMetrics": false, + "emitOSLogs": false, + "formatVersion": 2, + "gatherLocalizableStringsData": false, + "initializeForUITesting": true, + "maximumTestExecutionTimeAllowance": nil, + "productModuleName": "WebDriverAgentRunner", // set to other value is also OK + "randomExecutionOrderingSeed": nil, + "reportActivities": true, + "reportResultsToIDE": true, + "systemAttachmentLifetime": 2, + "targetApplicationArguments": []interface{}{}, // maybe useless + "targetApplicationEnvironment": nil, + "targetApplicationPath": targetAppPath, + "testApplicationDependencies": map[string]interface{}{}, + "testApplicationUserOverrides": nil, + "testBundleRelativePath": nil, + "testExecutionOrdering": 0, + "testTimeoutsEnabled": false, + "testsDrivenByIDE": false, + "testsMustRunOnMainThread": true, + "testsToRun": nil, + "testsToSkip": nil, + "treatMissingBaselinesAsFailures": false, + "userAttachmentLifetime": 1, + "testBundleURL": nsurl, + "sessionIdentifier": nsuuid, + "targetApplicationBundleID": targetBundleID, + // "targetApplicationBundleID": "", + } + return &XCTestConfiguration{internal: contents} +} + +func (cfg *XCTestConfiguration) archive(objects []interface{}) []interface{} { + info := map[string]interface{}{} + objects = append(objects, info) + + info["$class"] = plist.UID(len(objects)) + + cls := map[string]interface{}{ + "$classname": "XCTestConfiguration", + "$classes": []interface{}{"XCTestConfiguration", "NSObject"}, + } + objects = append(objects, cls) + + for k, v := range cfg.internal { + val := reflect.ValueOf(v) + if !val.IsValid() { + info[k] = plist.UID(0) + continue + } + + typ := val.Type() + + if k != "formatVersion" && (typ.Kind() == reflect.Bool || typ.Kind() == reflect.Uintptr || typ.Kind() == reflect.Int) { + info[k] = v + } else { + var uid plist.UID + objects, uid = archive(objects, v) + info[k] = uid + } + } + return objects +} diff --git a/hrp/pkg/gidevice/pkg/nskeyedarchiver/xctestconfiguration_test.go b/hrp/pkg/gidevice/pkg/nskeyedarchiver/xctestconfiguration_test.go new file mode 100644 index 00000000..c68f433e --- /dev/null +++ b/hrp/pkg/gidevice/pkg/nskeyedarchiver/xctestconfiguration_test.go @@ -0,0 +1,15 @@ +package nskeyedarchiver + +import ( + "fmt" + "testing" + + uuid "github.com/satori/go.uuid" +) + +func TestXCTestConfiguration_archive(t *testing.T) { + objs := make([]interface{}, 0, 1) + xcTestConfiguration := NewXCTestConfiguration(NewNSUUID(uuid.NewV4().Bytes()), NewNSURL("/tmp"), "", "") + objects := xcTestConfiguration.archive(objs) + fmt.Println(objects) +} diff --git a/hrp/pkg/gidevice/screenshot.go b/hrp/pkg/gidevice/screenshot.go new file mode 100644 index 00000000..1df1847b --- /dev/null +++ b/hrp/pkg/gidevice/screenshot.go @@ -0,0 +1,119 @@ +package gidevice + +import ( + "bytes" + "errors" + "fmt" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +var _ Screenshot = (*screenshot)(nil) + +func newScreenshot(client *libimobiledevice.ScreenshotClient) *screenshot { + return &screenshot{ + client: client, + exchanged: false, + } +} + +type screenshot struct { + client *libimobiledevice.ScreenshotClient + exchanged bool +} + +func (s *screenshot) Take() (raw *bytes.Buffer, err error) { + if err = s.exchange(); err != nil { + return nil, err + } + + // link service + req := []interface{}{ + "DLMessageProcessMessage", + map[string]interface{}{ + "MessageType": "ScreenShotRequest", + }, + } + + var pkt libimobiledevice.Packet + if pkt, err = s.client.NewBinaryPacket(req); err != nil { + return nil, err + } + + if err = s.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = s.client.ReceivePacket(); err != nil { + return nil, err + } + + var resp []interface{} + if err = respPkt.Unmarshal(&resp); err != nil { + return nil, err + } + + if resp[0].(string) != "DLMessageProcessMessage" { + return nil, fmt.Errorf("message device not ready %s %s", resp[3], resp[4]) + } + + raw = new(bytes.Buffer) + + screen := resp[1].(map[string]interface{}) + var data []byte + ok := false + if data, ok = screen["ScreenShotData"].([]byte); !ok { + return nil, errors.New("`ScreenShotData` not ready") + } + if _, err = raw.Write(data); err != nil { + return nil, err + } + return +} + +func (s *screenshot) exchange() (err error) { + if s.exchanged { + return + } + + var respPkt libimobiledevice.Packet + if respPkt, err = s.client.ReceivePacket(); err != nil { + return err + } + + var resp []interface{} + if err = respPkt.Unmarshal(&resp); err != nil { + return err + } + + req := []interface{}{ + "DLMessageVersionExchange", + "DLVersionsOk", + resp[1], + } + + var pkt libimobiledevice.Packet + if pkt, err = s.client.NewBinaryPacket(req); err != nil { + return err + } + + if err = s.client.SendPacket(pkt); err != nil { + return err + } + + if respPkt, err = s.client.ReceivePacket(); err != nil { + return err + } + + if err = respPkt.Unmarshal(&resp); err != nil { + return err + } + + if resp[3].(string) != "DLMessageDeviceReady" { + return fmt.Errorf("message device not ready %s", resp[3]) + } + + s.exchanged = true + return +} diff --git a/hrp/pkg/gidevice/screenshot_test.go b/hrp/pkg/gidevice/screenshot_test.go new file mode 100644 index 00000000..eb751fbd --- /dev/null +++ b/hrp/pkg/gidevice/screenshot_test.go @@ -0,0 +1,56 @@ +package gidevice + +import ( + "image" + "image/jpeg" + "image/png" + "os" + "testing" +) + +var screenshotSrv Screenshot + +func setupScreenshotSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if screenshotSrv, err = lockdownSrv.ScreenshotService(); err != nil { + t.Fatal(err) + } +} + +func Test_screenshot_Take(t *testing.T) { + setupScreenshotSrv(t) + + // raw, err := dev.Screenshot() + raw, err := screenshotSrv.Take() + if err != nil { + t.Fatal(err) + } + _ = raw + + img, format, err := image.Decode(raw) + if err != nil { + t.Fatal(err) + } + userHomeDir, _ := os.UserHomeDir() + file, err := os.Create(userHomeDir + "/Desktop/s1." + format) + if err != nil { + t.Fatal(err) + } + defer func() { _ = file.Close() }() + switch format { + case "png": + err = png.Encode(file, img) + case "jpeg": + err = jpeg.Encode(file, img, nil) + } + if err != nil { + t.Fatal(err) + } + t.Log(file.Name()) +} diff --git a/hrp/pkg/gidevice/simulatelocation.go b/hrp/pkg/gidevice/simulatelocation.go new file mode 100644 index 00000000..9381d06a --- /dev/null +++ b/hrp/pkg/gidevice/simulatelocation.go @@ -0,0 +1,27 @@ +package gidevice + +import "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" + +var _ SimulateLocation = (*simulateLocation)(nil) + +func newSimulateLocation(client *libimobiledevice.SimulateLocationClient) *simulateLocation { + return &simulateLocation{ + client: client, + } +} + +type simulateLocation struct { + client *libimobiledevice.SimulateLocationClient +} + +func (s *simulateLocation) Update(longitude float64, latitude float64, coordinateSystem ...CoordinateSystem) (err error) { + if len(coordinateSystem) == 0 { + coordinateSystem = []CoordinateSystem{CoordinateSystemWGS84} + } + pkt := s.client.NewLocationPacket(longitude, latitude, coordinateSystem[0]) + return s.client.SendPacket(pkt) +} + +func (s *simulateLocation) Recover() (err error) { + return s.client.Recover() +} diff --git a/hrp/pkg/gidevice/simulatelocation_test.go b/hrp/pkg/gidevice/simulatelocation_test.go new file mode 100644 index 00000000..df6833ef --- /dev/null +++ b/hrp/pkg/gidevice/simulatelocation_test.go @@ -0,0 +1,48 @@ +package gidevice + +import "testing" + +var simulateLocationSrv SimulateLocation + +func setupSimulateLocationSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if simulateLocationSrv, err = lockdownSrv.SimulateLocationService(); err != nil { + t.Fatal(err) + } +} + +func Test_simulateLocation_Update(t *testing.T) { + setupSimulateLocationSrv(t) + + // https://api.map.baidu.com/lbsapi/getpoint/index.html + // if err := dev.SimulateLocationUpdate(116.024067, 40.362639, CoordinateSystemBD09); err != nil { + if err := simulateLocationSrv.Update(116.024067, 40.362639, CoordinateSystemBD09); err != nil { + t.Fatal(err) + } + + // https://developer.amap.com/tools/picker + // https://lbs.qq.com/tool/getpoint/index.html + // if err := simulateLocationSrv.Update(120.116979,30.252876, CoordinateSystemGCJ02); err != nil { + // t.Fatal(err) + // } + + if err := simulateLocationSrv.Update(121.499763, 31.239580); err != nil { + // if err := simulateLocationSrv.Update(121.499763, 31.239580, CoordinateSystemWGS84); err != nil { + t.Fatal(err) + } +} + +func Test_simulateLocation_Recover(t *testing.T) { + setupSimulateLocationSrv(t) + + // if err := dev.SimulateLocationRecover(); err != nil { + if err := simulateLocationSrv.Recover(); err != nil { + t.Fatal(err) + } +} diff --git a/hrp/pkg/gidevice/springboard.go b/hrp/pkg/gidevice/springboard.go new file mode 100644 index 00000000..5843d86a --- /dev/null +++ b/hrp/pkg/gidevice/springboard.go @@ -0,0 +1,68 @@ +package gidevice + +import ( + "bytes" + "fmt" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +func newSpringBoard(client *libimobiledevice.SpringBoardClient) *springboard { + return &springboard{ + client: client, + } +} + +type springboard struct { + client *libimobiledevice.SpringBoardClient +} + +func (s springboard) GetIconPNGData(bundleId string) (raw *bytes.Buffer, err error) { + var pkt libimobiledevice.Packet + req := map[string]interface{}{ + "command": "getIconPNGData", + "bundleId": bundleId, + } + if pkt, err = s.client.NewBinaryPacket(req); err != nil { + return + } + if err = s.client.SendPacket(pkt); err != nil { + return nil, err + } + var respPkt libimobiledevice.Packet + if respPkt, err = s.client.ReceivePacket(); err != nil { + return nil, err + } + var reply libimobiledevice.IconPNGDataResponse + raw = new(bytes.Buffer) + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, fmt.Errorf("receive packet: %w", err) + } + if _, err = raw.Write(reply.PNGData); err != nil { + return nil, err + } + return +} + +func (s springboard) GetInterfaceOrientation() (orientation libimobiledevice.OrientationState, err error) { + var pkt libimobiledevice.Packet + req := map[string]interface{}{ + "command": "getInterfaceOrientation", + } + if pkt, err = s.client.NewBinaryPacket(req); err != nil { + return + } + if err = s.client.SendPacket(pkt); err != nil { + return 0, err + } + var respPkt libimobiledevice.Packet + if respPkt, err = s.client.ReceivePacket(); err != nil { + return 0, err + } + var reply libimobiledevice.InterfaceOrientationResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return 0, fmt.Errorf("receive packet: %w", err) + } + orientation = reply.Orientation + return +} diff --git a/hrp/pkg/gidevice/springboard_test.go b/hrp/pkg/gidevice/springboard_test.go new file mode 100644 index 00000000..98cc034a --- /dev/null +++ b/hrp/pkg/gidevice/springboard_test.go @@ -0,0 +1,53 @@ +package gidevice + +import ( + "fmt" + "image" + "image/jpeg" + "image/png" + "os" + "testing" +) + +var springBoardSrv SpringBoard + +func setupSpringBoardSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if springBoardSrv, err = lockdownSrv.SpringBoardService(); err != nil { + t.Fatal(err) + } +} + +func Test_springBoard_GetIcon(t *testing.T) { + setupSpringBoardSrv(t) + raw, _ := springBoardSrv.GetIconPNGData("com.tencent.xin") + img, format, err := image.Decode(raw) + if err != nil { + t.Fatal(err) + } + file, err := os.Create("./abc." + format) + if err != nil { + t.Fatal(err) + } + defer func() { _ = file.Close() }() + switch format { + case "png": + err = png.Encode(file, img) + case "jpeg": + err = jpeg.Encode(file, img, nil) + } + if err != nil { + t.Fatal(err) + } +} + +func Test_springBoard_GetOrient(t *testing.T) { + setupSpringBoardSrv(t) + fmt.Println(springBoardSrv.GetInterfaceOrientation()) +} diff --git a/hrp/pkg/gidevice/syslogrelay.go b/hrp/pkg/gidevice/syslogrelay.go new file mode 100644 index 00000000..0b025721 --- /dev/null +++ b/hrp/pkg/gidevice/syslogrelay.go @@ -0,0 +1,85 @@ +package gidevice + +import ( + "bufio" + "fmt" + "io" + "strings" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +var _ SyslogRelay = (*syslogRelay)(nil) + +func newSyslogRelay(client *libimobiledevice.SyslogRelayClient) *syslogRelay { + r := &syslogRelay{ + client: client, + stop: make(chan bool), + isReading: false, + } + r.reader = bufio.NewReader(r.client.InnerConn().RawConn()) + return r +} + +type syslogRelay struct { + client *libimobiledevice.SyslogRelayClient + + reader *bufio.Reader + stop chan bool + isReading bool +} + +func (r *syslogRelay) Lines() <-chan string { + out := make(chan string) + r.isReading = true + + go func() { + defer func() { + close(out) + r.isReading = false + }() + for { + select { + case <-r.stop: + return + default: + bs, err := r.readLine() + if err != nil { + if strings.Contains(err.Error(), io.EOF.Error()) { + return + } + debugLog(fmt.Sprintf("syslog: %s", err)) + } + if len(bs) > 1 && bs[0] == 0 { + bs = bs[1:] + } + out <- string(bs) + } + } + }() + return out +} + +func (r *syslogRelay) Stop() { + if r.isReading { + r.stop <- true + } +} + +func (r *syslogRelay) readLine() ([]byte, error) { + var line []byte + for { + l, more, err := r.reader.ReadLine() + if err != nil { + return nil, err + } + if line == nil && !more { + return l, nil + } + line = append(line, l...) + if !more { + break + } + } + return line, nil +} diff --git a/hrp/pkg/gidevice/testmanagerd.go b/hrp/pkg/gidevice/testmanagerd.go new file mode 100644 index 00000000..61eb2726 --- /dev/null +++ b/hrp/pkg/gidevice/testmanagerd.go @@ -0,0 +1,50 @@ +package gidevice + +import ( + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +var _ Testmanagerd = (*testmanagerd)(nil) + +func newTestmanagerd(client *libimobiledevice.TestmanagerdClient, iOSVersion []int) *testmanagerd { + return &testmanagerd{ + client: client, + iOSVersion: iOSVersion, + } +} + +type testmanagerd struct { + client *libimobiledevice.TestmanagerdClient + iOSVersion []int +} + +func (t *testmanagerd) notifyOfPublishedCapabilities() (err error) { + _, err = t.client.Connection() + return +} + +func (t *testmanagerd) requestChannel(channel string) (id uint32, err error) { + return t.client.MakeChannel(channel) +} + +func (t *testmanagerd) newXCTestManagerDaemon() (xcTestManager XCTestManagerDaemon, err error) { + var channelCode uint32 + if channelCode, err = t.requestChannel("dtxproxy:XCTestManager_IDEInterface:XCTestManager_DaemonConnectionInterface"); err != nil { + return nil, err + } + xcTestManager = newXcTestManagerDaemon(t, channelCode) + return +} + +func (t *testmanagerd) invoke(selector string, args *libimobiledevice.AuxBuffer, channelCode uint32, expectsReply bool) ( + result *libimobiledevice.DTXMessageResult, err error) { + return t.client.Invoke(selector, args, channelCode, expectsReply) +} + +func (t *testmanagerd) registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) { + t.client.RegisterCallback(obj, cb) +} + +func (t *testmanagerd) close() { + t.client.Close() +} diff --git a/hrp/pkg/gidevice/usbmux.go b/hrp/pkg/gidevice/usbmux.go new file mode 100644 index 00000000..e0e60347 --- /dev/null +++ b/hrp/pkg/gidevice/usbmux.go @@ -0,0 +1,153 @@ +package gidevice + +import ( + "context" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +var _ Usbmux = (*usbmux)(nil) + +func NewUsbmux() (Usbmux, error) { + umClient, err := libimobiledevice.NewUsbmuxClient() + if err != nil { + return nil, err + } + return &usbmux{client: umClient}, nil +} + +func newUsbmux(client *libimobiledevice.UsbmuxClient) *usbmux { + return &usbmux{client: client} +} + +type usbmux struct { + client *libimobiledevice.UsbmuxClient +} + +func (um *usbmux) Devices() (devices []Device, err error) { + var pkt libimobiledevice.Packet + if pkt, err = um.client.NewPlistPacket( + um.client.NewBasicRequest(libimobiledevice.MessageTypeDeviceList), + ); err != nil { + return nil, err + } + + if err = um.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = um.client.ReceivePacket(); err != nil { + return nil, err + } + + reply := struct { + DeviceList []libimobiledevice.BaseDevice `plist:"DeviceList"` + }{} + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + + devices = make([]Device, len(reply.DeviceList)) + for i := range reply.DeviceList { + dev := reply.DeviceList[i] + devices[i] = newDevice(um.client, dev.Properties) + } + + return +} + +func (um *usbmux) ReadBUID() (buid string, err error) { + var pktReadBUID libimobiledevice.Packet + if pktReadBUID, err = um.client.NewPlistPacket( + um.client.NewBasicRequest(libimobiledevice.MessageTypeReadBUID), + ); err != nil { + return "", err + } + + if err = um.client.SendPacket(pktReadBUID); err != nil { + return "", err + } + + respPkt, err := um.client.ReceivePacket() + if err != nil { + return "", err + } + + reply := struct { + BUID string `plist:"BUID"` + }{} + if err = respPkt.Unmarshal(&reply); err != nil { + return "", err + } + + buid = reply.BUID + + return +} + +func (um *usbmux) Listen(devNotifier chan Device) (context.CancelFunc, error) { + baseDevNotifier := make(chan libimobiledevice.BaseDevice) + ctx, cancelFunc, err := um.listen(baseDevNotifier) + go func(ctx context.Context) { + defer close(devNotifier) + for { + select { + case <-ctx.Done(): + return + case baseDev := <-baseDevNotifier: + if baseDev.MessageType != libimobiledevice.MessageTypeDeviceAdd { + baseDev.Properties.DeviceID = baseDev.DeviceID + } + client, err := libimobiledevice.NewUsbmuxClient() + if err != nil { + continue + } + devNotifier <- newDevice(client, baseDev.Properties) + } + } + }(ctx) + return cancelFunc, err +} + +func (um *usbmux) listen(devNotifier chan libimobiledevice.BaseDevice) (ctx context.Context, cancelFunc context.CancelFunc, err error) { + var pkt libimobiledevice.Packet + if pkt, err = um.client.NewPlistPacket( + um.client.NewBasicRequest(libimobiledevice.MessageTypeListen), + ); err != nil { + return nil, nil, err + } + + if err = um.client.SendPacket(pkt); err != nil { + return nil, nil, err + } + + ctx, cancelFunc = context.WithCancel(context.Background()) + + go func(ctx context.Context) { + defer close(devNotifier) + for { + select { + case <-ctx.Done(): + return + default: + var respPkt libimobiledevice.Packet + if respPkt, err = um.client.ReceivePacket(); err != nil { + break + } + + var replyDevice libimobiledevice.BaseDevice + if err = respPkt.Unmarshal(&replyDevice); err != nil { + break + } + if replyDevice.MessageType == libimobiledevice.MessageTypeResult { + break + } + + devNotifier <- replyDevice + } + } + }(ctx) + + return ctx, cancelFunc, nil +} diff --git a/hrp/pkg/gidevice/usbmux_test.go b/hrp/pkg/gidevice/usbmux_test.go new file mode 100644 index 00000000..3509ce93 --- /dev/null +++ b/hrp/pkg/gidevice/usbmux_test.go @@ -0,0 +1,68 @@ +package gidevice + +import ( + "testing" + "time" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" +) + +var um Usbmux + +func setupUsbmux(t *testing.T) { + var err error + um, err = NewUsbmux() + if err != nil { + t.Fatal(err) + } +} + +func Test_usbmux_Devices(t *testing.T) { + setupUsbmux(t) + + devices, err := um.Devices() + if err != nil { + t.Fatal(err) + } + + for _, dev := range devices { + t.Log(dev.Properties().SerialNumber, dev.Properties().ProductID, dev.Properties().DeviceID) + } +} + +func Test_usbmux_ReadBUID(t *testing.T) { + setupUsbmux(t) + + buid, err := um.ReadBUID() + if err != nil { + t.Fatal(err) + } + + t.Log(buid) +} + +func Test_usbmux_Listen(t *testing.T) { + setupUsbmux(t) + + devNotifier := make(chan Device) + cancelFunc, err := um.Listen(devNotifier) + if err != nil { + t.Fatal(err) + } + + go func() { + time.Sleep(20 * time.Second) + cancelFunc() + }() + + for dev := range devNotifier { + if dev.Properties().ConnectionType != "" { + t.Log(dev.Properties().SerialNumber, dev.Properties().ProductID, dev.Properties().DeviceID) + } else { + t.Log(libimobiledevice.MessageTypeDeviceRemove, dev.Properties().DeviceID) + } + } + + time.Sleep(5 * time.Second) + t.Log("Done") +} diff --git a/hrp/pkg/gidevice/xctestmanagerdaemon.go b/hrp/pkg/gidevice/xctestmanagerdaemon.go new file mode 100644 index 00000000..9d65ff46 --- /dev/null +++ b/hrp/pkg/gidevice/xctestmanagerdaemon.go @@ -0,0 +1,153 @@ +package gidevice + +import ( + "fmt" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/nskeyedarchiver" +) + +var _ XCTestManagerDaemon = (*xcTestManagerDaemon)(nil) + +func newXcTestManagerDaemon(testmanagerd Testmanagerd, channelCode uint32) *xcTestManagerDaemon { + return &xcTestManagerDaemon{ + testmanagerd: testmanagerd, + channelCode: channelCode, + } +} + +type xcTestManagerDaemon struct { + testmanagerd Testmanagerd + channelCode uint32 +} + +func (d *xcTestManagerDaemon) initiateControlSession(XcodeVersion uint64) (err error) { + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(XcodeVersion); err != nil { + return err + } + + selector := "_IDE_initiateControlSessionWithProtocolVersion:" + + var ret *libimobiledevice.DTXMessageResult + if ret, err = d.testmanagerd.invoke(selector, args, d.channelCode, true); err != nil { + return err + } + + if nsErr, ok := ret.Obj.(libimobiledevice.NSError); ok { + return fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + return +} + +func (d *xcTestManagerDaemon) startExecutingTestPlan(XcodeVersion uint64) (err error) { + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(XcodeVersion); err != nil { + return err + } + + selector := "_IDE_startExecutingTestPlanWithProtocolVersion:" + + if _, err = d.testmanagerd.invoke(selector, args, 0xFFFFFFFF, false); err != nil { + return err + } + + return +} + +func (d *xcTestManagerDaemon) initiateSession(XcodeVersion uint64, nsUUID *nskeyedarchiver.NSUUID) (err error) { + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(nsUUID); err != nil { + return err + } + if err = args.AppendObject(nsUUID.String() + "-Go-iDevice"); err != nil { + return err + } + if err = args.AppendObject("/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild"); err != nil { + return err + } + if err = args.AppendObject(XcodeVersion); err != nil { + return err + } + + selector := "_IDE_initiateSessionWithIdentifier:forClient:atPath:protocolVersion:" + + var ret *libimobiledevice.DTXMessageResult + if ret, err = d.testmanagerd.invoke(selector, args, d.channelCode, true); err != nil { + return err + } + + if nsErr, ok := ret.Obj.(libimobiledevice.NSError); ok { + return fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + + return +} + +func (d *xcTestManagerDaemon) authorizeTestSession(pid int) (err error) { + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(pid); err != nil { + return err + } + + selector := "_IDE_authorizeTestSessionWithProcessID:" + + var ret *libimobiledevice.DTXMessageResult + if ret, err = d.testmanagerd.invoke(selector, args, d.channelCode, true); err != nil { + return err + } + + if nsErr, ok := ret.Obj.(libimobiledevice.NSError); ok { + return fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + return +} + +func (d *xcTestManagerDaemon) initiateControlSessionForTestProcessID(pid int) (err error) { + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(pid); err != nil { + return err + } + + selector := "_IDE_initiateControlSessionForTestProcessID:" + + var ret *libimobiledevice.DTXMessageResult + if ret, err = d.testmanagerd.invoke(selector, args, d.channelCode, true); err != nil { + return err + } + + if nsErr, ok := ret.Obj.(libimobiledevice.NSError); ok { + return fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + return +} + +func (d *xcTestManagerDaemon) initiateControlSessionForTestProcessIDProtocolVersion(pid int, XcodeVersion uint64) (err error) { + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(pid); err != nil { + return err + } + if err = args.AppendObject(XcodeVersion); err != nil { + return err + } + + selector := "_IDE_initiateControlSessionForTestProcessID:protocolVersion:" + + var ret *libimobiledevice.DTXMessageResult + if ret, err = d.testmanagerd.invoke(selector, args, d.channelCode, true); err != nil { + return err + } + + if nsErr, ok := ret.Obj.(libimobiledevice.NSError); ok { + return fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + return +} + +func (d *xcTestManagerDaemon) registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) { + d.testmanagerd.registerCallback(obj, cb) +} + +func (d *xcTestManagerDaemon) close() { + d.testmanagerd.close() +} diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index 0f8668c6..be21cebd 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -17,13 +17,13 @@ import ( "strings" "time" - giDevice "github.com/electricbubble/gidevice" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/json" + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" ) const ( @@ -96,18 +96,18 @@ func WithDismissAlertButtonSelector(selector string) IOSDeviceOption { } } -func WithPerfOptions(options ...giDevice.PerfOption) IOSDeviceOption { +func WithPerfOptions(options ...gidevice.PerfOption) IOSDeviceOption { return func(device *IOSDevice) { - device.PerfOptions = &giDevice.PerfOptions{} + device.PerfOptions = &gidevice.PerfOptions{} for _, option := range options { option(device.PerfOptions) } } } -func IOSDevices(udid ...string) (devices []giDevice.Device, err error) { - var usbmux giDevice.Usbmux - if usbmux, err = giDevice.NewUsbmux(); err != nil { +func IOSDevices(udid ...string) (devices []gidevice.Device, err error) { + var usbmux gidevice.Usbmux + if usbmux, err = gidevice.NewUsbmux(); err != nil { return nil, errors.Wrap(code.IOSDeviceConnectionError, fmt.Sprintf("init usbmux failed: %v", err)) } @@ -118,7 +118,7 @@ func IOSDevices(udid ...string) (devices []giDevice.Device, err error) { } // filter by udid - var deviceList []giDevice.Device + var deviceList []gidevice.Device for _, d := range devices { for _, u := range udid { if u != "" && u != d.Properties().SerialNumber { @@ -194,8 +194,8 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { } type IOSDevice struct { - d giDevice.Device - PerfOptions *giDevice.PerfOptions `json:"perf_options,omitempty" yaml:"perf_options,omitempty"` + d gidevice.Device + PerfOptions *gidevice.PerfOptions `json:"perf_options,omitempty" yaml:"perf_options,omitempty"` UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port @@ -297,7 +297,7 @@ func (dev *IOSDevice) forward(localPort, remotePort int) error { return err } - go func(listener net.Listener, device giDevice.Device) { + go func(listener net.Listener, device gidevice.Device) { for { accept, err := listener.Accept() if err != nil { @@ -332,53 +332,53 @@ func (dev *IOSDevice) forward(localPort, remotePort int) error { return nil } -func (dev *IOSDevice) perfOpitons() (perfOptions []giDevice.PerfOption) { +func (dev *IOSDevice) perfOpitons() (perfOptions []gidevice.PerfOption) { if dev.PerfOptions == nil { return } // system if dev.PerfOptions.SysCPU { - perfOptions = append(perfOptions, giDevice.WithPerfSystemCPU(true)) + perfOptions = append(perfOptions, gidevice.WithPerfSystemCPU(true)) } if dev.PerfOptions.SysMem { - perfOptions = append(perfOptions, giDevice.WithPerfSystemMem(true)) + perfOptions = append(perfOptions, gidevice.WithPerfSystemMem(true)) } if dev.PerfOptions.SysDisk { - perfOptions = append(perfOptions, giDevice.WithPerfSystemDisk(true)) + perfOptions = append(perfOptions, gidevice.WithPerfSystemDisk(true)) } if dev.PerfOptions.SysNetwork { - perfOptions = append(perfOptions, giDevice.WithPerfSystemNetwork(true)) + perfOptions = append(perfOptions, gidevice.WithPerfSystemNetwork(true)) } if dev.PerfOptions.FPS { - perfOptions = append(perfOptions, giDevice.WithPerfFPS(true)) + perfOptions = append(perfOptions, gidevice.WithPerfFPS(true)) } if dev.PerfOptions.Network { - perfOptions = append(perfOptions, giDevice.WithPerfNetwork(true)) + perfOptions = append(perfOptions, gidevice.WithPerfNetwork(true)) } // process if dev.PerfOptions.BundleID != "" { perfOptions = append(perfOptions, - giDevice.WithPerfBundleID(dev.PerfOptions.BundleID)) + gidevice.WithPerfBundleID(dev.PerfOptions.BundleID)) } if dev.PerfOptions.Pid != 0 { perfOptions = append(perfOptions, - giDevice.WithPerfPID(dev.PerfOptions.Pid)) + gidevice.WithPerfPID(dev.PerfOptions.Pid)) } // config if dev.PerfOptions.OutputInterval != 0 { perfOptions = append(perfOptions, - giDevice.WithPerfOutputInterval(dev.PerfOptions.OutputInterval)) + gidevice.WithPerfOutputInterval(dev.PerfOptions.OutputInterval)) } if dev.PerfOptions.SystemAttributes != nil { perfOptions = append(perfOptions, - giDevice.WithPerfSystemAttributes(dev.PerfOptions.SystemAttributes...)) + gidevice.WithPerfSystemAttributes(dev.PerfOptions.SystemAttributes...)) } if dev.PerfOptions.ProcessAttributes != nil { perfOptions = append(perfOptions, - giDevice.WithPerfProcessAttributes(dev.PerfOptions.ProcessAttributes...)) + gidevice.WithPerfProcessAttributes(dev.PerfOptions.ProcessAttributes...)) } return } diff --git a/hrp/pkg/uixt/ios_driver.go b/hrp/pkg/uixt/ios_driver.go index 72da3928..4796d577 100644 --- a/hrp/pkg/uixt/ios_driver.go +++ b/hrp/pkg/uixt/ios_driver.go @@ -11,22 +11,22 @@ import ( "strings" "time" - giDevice "github.com/electricbubble/gidevice" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/json" + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" ) type wdaDriver struct { Driver // default port - defaultConn giDevice.InnerConn + defaultConn gidevice.InnerConn // mjpeg port - mjpegUSBConn giDevice.InnerConn // via USB + mjpegUSBConn gidevice.InnerConn // via USB mjpegHTTPConn net.Conn // via HTTP mjpegClient *http.Client } diff --git a/hrp/step.go b/hrp/step.go index b721faea..8adb9e7d 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -1,8 +1,7 @@ package hrp import ( - giDevice "github.com/electricbubble/gidevice" - + "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice" "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) @@ -36,18 +35,18 @@ var ( ) var ( - WithPerfSystemCPU = giDevice.WithPerfSystemCPU - WithPerfSystemMem = giDevice.WithPerfSystemMem - WithPerfSystemDisk = giDevice.WithPerfSystemDisk - WithPerfSystemNetwork = giDevice.WithPerfSystemNetwork - WithPerfGPU = giDevice.WithPerfGPU - WithPerfFPS = giDevice.WithPerfFPS - WithPerfNetwork = giDevice.WithPerfNetwork - WithPerfBundleID = giDevice.WithPerfBundleID - WithPerfPID = giDevice.WithPerfPID - WithPerfOutputInterval = giDevice.WithPerfOutputInterval - WithPerfProcessAttributes = giDevice.WithPerfProcessAttributes - WithPerfSystemAttributes = giDevice.WithPerfSystemAttributes + WithPerfSystemCPU = gidevice.WithPerfSystemCPU + WithPerfSystemMem = gidevice.WithPerfSystemMem + WithPerfSystemDisk = gidevice.WithPerfSystemDisk + WithPerfSystemNetwork = gidevice.WithPerfSystemNetwork + WithPerfGPU = gidevice.WithPerfGPU + WithPerfFPS = gidevice.WithPerfFPS + WithPerfNetwork = gidevice.WithPerfNetwork + WithPerfBundleID = gidevice.WithPerfBundleID + WithPerfPID = gidevice.WithPerfPID + WithPerfOutputInterval = gidevice.WithPerfOutputInterval + WithPerfProcessAttributes = gidevice.WithPerfProcessAttributes + WithPerfSystemAttributes = gidevice.WithPerfSystemAttributes ) type StepResult struct {