Files
httprunner/hrp/pkg/uixt/ios_device.go
2024-10-16 15:32:00 +08:00

774 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package uixt
import (
"context"
"fmt"
"io"
builtinLog "log"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"time"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/code"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/env"
"github.com/httprunner/httprunner/v4/hrp/pkg/gidevice"
)
const (
defaultWDAPort = 8100
defaultMjpegPort = 9100
)
const (
// Changes the value of maximum depth for traversing elements source tree.
// It may help to prevent out of memory or timeout errors while getting the elements source tree,
// but it might restrict the depth of source tree.
// A part of elements source tree might be lost if the value was too small. Defaults to 50
snapshotMaxDepth = 10
// Allows to customize accept/dismiss alert button selector.
// It helps you to handle an arbitrary element as accept button in accept alert command.
// The selector should be a valid class chain expression, where the search root is the alert element itself.
// The default button location algorithm is used if the provided selector is wrong or does not match any element.
// e.g. **/XCUIElementTypeButton[`label CONTAINS[c] accept`]
acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]"
dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]"
)
type IOSPerfOption = gidevice.PerfOption
var (
WithIOSPerfSystemCPU = gidevice.WithPerfSystemCPU
WithIOSPerfSystemMem = gidevice.WithPerfSystemMem
WithIOSPerfSystemDisk = gidevice.WithPerfSystemDisk
WithIOSPerfSystemNetwork = gidevice.WithPerfSystemNetwork
WithIOSPerfGPU = gidevice.WithPerfGPU
WithIOSPerfFPS = gidevice.WithPerfFPS
WithIOSPerfNetwork = gidevice.WithPerfNetwork
WithIOSPerfBundleID = gidevice.WithPerfBundleID
WithIOSPerfPID = gidevice.WithPerfPID
WithIOSPerfOutputInterval = gidevice.WithPerfOutputInterval
WithIOSPerfProcessAttributes = gidevice.WithPerfProcessAttributes
WithIOSPerfSystemAttributes = gidevice.WithPerfSystemAttributes
)
type IOSPcapOption = gidevice.PcapOption
var (
WithIOSPcapAll = gidevice.WithPcapAll
WithIOSPcapPID = gidevice.WithPcapPID
WithIOSPcapProcName = gidevice.WithPcapProcName
WithIOSPcapBundleID = gidevice.WithPcapBundleID
)
type IOSDeviceOption func(*IOSDevice)
func WithUDID(udid string) IOSDeviceOption {
return func(device *IOSDevice) {
device.UDID = udid
}
}
func WithWDAPort(port int) IOSDeviceOption {
return func(device *IOSDevice) {
device.Port = port
}
}
func WithWDAMjpegPort(port int) IOSDeviceOption {
return func(device *IOSDevice) {
device.MjpegPort = port
}
}
func WithWDALogOn(logOn bool) IOSDeviceOption {
return func(device *IOSDevice) {
device.LogOn = logOn
}
}
func WithResetHomeOnStartup(reset bool) IOSDeviceOption {
return func(device *IOSDevice) {
device.ResetHomeOnStartup = reset
}
}
func WithSnapshotMaxDepth(depth int) IOSDeviceOption {
return func(device *IOSDevice) {
device.SnapshotMaxDepth = depth
}
}
func WithAcceptAlertButtonSelector(selector string) IOSDeviceOption {
return func(device *IOSDevice) {
device.AcceptAlertButtonSelector = selector
}
}
func WithDismissAlertButtonSelector(selector string) IOSDeviceOption {
return func(device *IOSDevice) {
device.DismissAlertButtonSelector = selector
}
}
func WithXCTest(bundleID string) IOSDeviceOption {
return func(device *IOSDevice) {
device.XCTestBundleID = bundleID
}
}
func WithIOSPerfOptions(options ...gidevice.PerfOption) IOSDeviceOption {
return func(device *IOSDevice) {
device.PerfOptions = &gidevice.PerfOptions{}
for _, option := range options {
option(device.PerfOptions)
}
}
}
func WithIOSPcapOptions(options ...gidevice.PcapOption) IOSDeviceOption {
return func(device *IOSDevice) {
device.PcapOptions = &gidevice.PcapOptions{}
for _, option := range options {
option(device.PcapOptions)
}
}
}
func GetIOSDevices(udid ...string) (devices []gidevice.Device, err error) {
var usbmux gidevice.Usbmux
if usbmux, err = gidevice.NewUsbmux(); err != nil {
return nil, errors.Wrap(code.DeviceConnectionError,
fmt.Sprintf("init usbmux failed: %v", err))
}
if devices, err = usbmux.Devices(); err != nil {
return nil, errors.Wrap(code.DeviceConnectionError,
fmt.Sprintf("list ios devices failed: %v", err))
}
// filter by udid
var deviceList []gidevice.Device
for _, d := range devices {
for _, u := range udid {
if u != "" && u != d.Properties().SerialNumber {
continue
}
// filter non-usb ios devices
if d.Properties().ConnectionType != "USB" {
continue
}
deviceList = append(deviceList, d)
}
}
if len(deviceList) == 0 {
var err error
if udid == nil || (len(udid) == 1 && udid[0] == "") {
err = fmt.Errorf("no ios device found")
} else {
err = fmt.Errorf("no ios device found for udid %v", udid)
}
return nil, err
}
return deviceList, nil
}
func GetIOSDeviceOptions(dev *IOSDevice) (deviceOptions []IOSDeviceOption) {
if dev.UDID != "" {
deviceOptions = append(deviceOptions, WithUDID(dev.UDID))
}
if dev.Port != 0 {
deviceOptions = append(deviceOptions, WithWDAPort(dev.Port))
}
if dev.MjpegPort != 0 {
deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.MjpegPort))
}
if dev.LogOn {
deviceOptions = append(deviceOptions, WithWDALogOn(true))
}
if dev.PerfOptions != nil {
deviceOptions = append(deviceOptions, WithIOSPerfOptions(dev.perfOpitons()...))
}
if dev.PcapOptions != nil {
deviceOptions = append(deviceOptions, WithIOSPcapOptions(dev.pcapOpitons()...))
}
if dev.XCTestBundleID != "" {
deviceOptions = append(deviceOptions, WithXCTest(dev.XCTestBundleID))
}
if dev.ResetHomeOnStartup {
deviceOptions = append(deviceOptions, WithResetHomeOnStartup(true))
}
if dev.SnapshotMaxDepth != 0 {
deviceOptions = append(deviceOptions, WithSnapshotMaxDepth(dev.SnapshotMaxDepth))
}
if dev.AcceptAlertButtonSelector != "" {
deviceOptions = append(deviceOptions, WithAcceptAlertButtonSelector(dev.AcceptAlertButtonSelector))
}
if dev.DismissAlertButtonSelector != "" {
deviceOptions = append(deviceOptions, WithDismissAlertButtonSelector(dev.DismissAlertButtonSelector))
}
return
}
func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) {
device = &IOSDevice{
Port: defaultWDAPort,
MjpegPort: defaultMjpegPort,
SnapshotMaxDepth: snapshotMaxDepth,
AcceptAlertButtonSelector: acceptAlertButtonSelector,
DismissAlertButtonSelector: dismissAlertButtonSelector,
// switch to iOS springboard before init WDA session
// avoid getting stuck when some super app is active such as douyin or wexin
ResetHomeOnStartup: true,
}
for _, option := range options {
option(device)
}
deviceList, err := GetIOSDevices(device.UDID)
if err != nil {
return nil, errors.Wrap(code.DeviceConnectionError, err.Error())
}
if device.UDID == "" && len(deviceList) > 1 {
return nil, errors.Wrap(code.DeviceConnectionError, "more than one device connected, please specify the udid")
}
dev := deviceList[0]
udid := dev.Properties().SerialNumber
if device.UDID == "" {
device.UDID = udid
log.Warn().
Str("udid", udid).
Msg("ios UDID is not specified, select the first one")
}
device.d = dev
// run xctest if XCTestBundleID is set
if device.XCTestBundleID != "" {
_, err = device.RunXCTest(device.XCTestBundleID)
if err != nil {
log.Error().Err(err).Str("udid", udid).Msg("failed to init XCTest")
return
}
}
log.Info().Str("udid", device.UDID).Msg("init ios device")
return device, nil
}
type IOSDevice struct {
d gidevice.Device
PerfOptions *gidevice.PerfOptions `json:"perf_options,omitempty" yaml:"perf_options,omitempty"`
PcapOptions *gidevice.PcapOptions `json:"pcap_options,omitempty" yaml:"pcap_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
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
XCTestBundleID string `json:"xctest_bundle_id,omitempty" yaml:"xctest_bundle_id,omitempty"`
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"`
// switch to iOS springboard before init WDA session
ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"`
// config appium settings
SnapshotMaxDepth int `json:"snapshot_max_depth,omitempty" yaml:"snapshot_max_depth,omitempty"`
AcceptAlertButtonSelector string `json:"accept_alert_button_selector,omitempty" yaml:"accept_alert_button_selector,omitempty"`
DismissAlertButtonSelector string `json:"dismiss_alert_button_selector,omitempty" yaml:"dismiss_alert_button_selector,omitempty"`
// performance monitor
perfStop chan struct{} // stop performance monitor
perfFile string // saved perf file path
// pcap monitor
pcapStop chan struct{} // stop pcap monitor
pcapFile string // saved pcap file path
}
func (dev *IOSDevice) Init() error {
return nil
}
func (dev *IOSDevice) UUID() string {
return dev.UDID
}
func (dev *IOSDevice) LogEnabled() bool {
return dev.LogOn
}
func (dev *IOSDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) {
driverOptions := NewDriverOptions()
for _, option := range options {
option(driverOptions)
}
// init WDA driver
capabilities := driverOptions.capabilities
if capabilities == nil {
capabilities = NewCapabilities()
capabilities.WithDefaultAlertAction(AlertActionAccept)
}
var driver IWebDriver
if os.Getenv("WDA_USB_DRIVER") == "" {
// default use http driver
driver, err = dev.NewHTTPDriver(capabilities)
} else {
driver, err = dev.NewUSBDriver(capabilities)
}
if err != nil {
return nil, errors.Wrap(err, "failed to init WDA driver")
}
if dev.ResetHomeOnStartup {
log.Info().Msg("go back to home screen")
if err = driver.Homescreen(); err != nil {
return nil, errors.Wrap(code.MobileUIDriverError,
fmt.Sprintf("go back to home screen failed: %v", err))
}
}
driverExt, err = newDriverExt(dev, driver, options...)
if err != nil {
return nil, err
}
settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{
"snapshotMaxDepth": dev.SnapshotMaxDepth,
"acceptAlertButtonSelector": dev.AcceptAlertButtonSelector,
})
if err != nil {
return nil, errors.Wrap(err, "failed to set appium WDA settings")
}
log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings")
if dev.LogOn {
err = driverExt.Driver.StartCaptureLog("hrp_wda_log")
if err != nil {
return nil, err
}
}
if dev.PerfOptions != nil {
if err := dev.StartPerf(); err != nil {
return nil, err
}
}
if dev.PcapOptions != nil {
if err := dev.StartPcap(); err != nil {
return nil, err
}
}
return driverExt, nil
}
func (dev *IOSDevice) StartPerf() error {
log.Info().Msg("start performance monitor")
data, err := dev.d.PerfStart(dev.perfOpitons()...)
if err != nil {
return err
}
dev.perfFile = filepath.Join(env.ResultsPath, "perf.data")
file, err := os.OpenFile(dev.perfFile,
os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o755)
if err != nil {
return err
}
dev.perfStop = make(chan struct{})
// start performance monitor
go func() {
for {
select {
case <-dev.perfStop:
file.Close()
dev.d.PerfStop()
return
case d := <-data:
_, err = file.WriteString(string(d) + "\n")
if err != nil {
log.Error().Err(err).
Str("line", string(d)).
Msg("write perf data failed")
}
}
}
}()
return nil
}
func (dev *IOSDevice) StopPerf() string {
if dev.perfStop == nil {
return ""
}
close(dev.perfStop)
log.Info().Str("perfFile", dev.perfFile).Msg("stop performance monitor")
return dev.perfFile
}
func (dev *IOSDevice) StartPcap() error {
log.Info().Msg("start packet capture")
packets, err := dev.d.PcapStart(dev.pcapOpitons()...)
if err != nil {
return err
}
dev.pcapFile = filepath.Join(env.ResultsPath, "dump.pcap")
file, err := os.OpenFile(dev.pcapFile,
os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o755)
if err != nil {
return err
}
// pcap magic number
// https://www.ietf.org/archive/id/draft-gharris-opsawg-pcap-01.html
_, _ = file.Write([]byte{
0xd4, 0xc3, 0xb2, 0xa1, 0x02, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
})
dev.pcapStop = make(chan struct{})
// start pcap monitor
go func() {
for {
select {
case <-dev.pcapStop:
file.Close()
dev.d.PcapStop()
return
case d := <-packets:
_, err = file.Write(d)
if err != nil {
log.Error().Err(err).Msg("write pcap data failed")
}
}
}
}()
return nil
}
// StopPcap stops pcap monitor and returns the saved pcap file path
func (dev *IOSDevice) StopPcap() string {
if dev.pcapStop == nil {
return ""
}
close(dev.pcapStop)
log.Info().Str("pcapFile", dev.pcapFile).Msg("stop packet capture")
return dev.pcapFile
}
func (dev *IOSDevice) Install(appPath string, opts *InstallOptions) (err error) {
for i := 0; i <= opts.RetryTime; i++ {
err = builtin.RunCommand("go-ios", "install", "--path="+appPath, "--udid="+dev.UDID)
if err == nil {
return nil
}
}
return err
}
func (dev *IOSDevice) Uninstall(bundleId string) error {
return builtin.RunCommand("go-ios", "uninstall", bundleId, "--udid="+dev.UDID)
}
func (dev *IOSDevice) forward(localPort, remotePort int) error {
log.Info().Int("localPort", localPort).Int("remotePort", remotePort).
Str("udid", dev.UDID).Msg("forward tcp port")
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", localPort))
if err != nil {
log.Error().Err(err).Msg("listen tcp error")
return err
}
go func(listener net.Listener, device gidevice.Device) {
for {
accept, err := listener.Accept()
if err != nil {
log.Error().Err(err).Msg("accept error")
continue
}
rInnerConn, err := device.NewConnect(remotePort)
if err != nil {
log.Error().Err(err).Msg("connect to ios device failed")
os.Exit(code.GetErrorCode(code.DeviceConnectionError))
}
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 {
log.Error().Err(err).Msg("copy local -> remote")
rConn.Close()
accept.Close()
}
}(lConn, rConn)
go func(lConn, rConn net.Conn) {
if _, err := io.Copy(rConn, lConn); err != nil {
log.Error().Err(err).Msg("copy local <- remote")
rConn.Close()
accept.Close()
}
}(lConn, rConn)
}(accept)
}
}(listener, dev.d)
return nil
}
func (dev *IOSDevice) perfOpitons() (perfOptions []gidevice.PerfOption) {
if dev.PerfOptions == nil {
return
}
// system
if dev.PerfOptions.SysCPU {
perfOptions = append(perfOptions, gidevice.WithPerfSystemCPU(true))
}
if dev.PerfOptions.SysMem {
perfOptions = append(perfOptions, gidevice.WithPerfSystemMem(true))
}
if dev.PerfOptions.SysDisk {
perfOptions = append(perfOptions, gidevice.WithPerfSystemDisk(true))
}
if dev.PerfOptions.SysNetwork {
perfOptions = append(perfOptions, gidevice.WithPerfSystemNetwork(true))
}
if dev.PerfOptions.FPS {
perfOptions = append(perfOptions, gidevice.WithPerfFPS(true))
}
if dev.PerfOptions.Network {
perfOptions = append(perfOptions, gidevice.WithPerfNetwork(true))
}
// process
if dev.PerfOptions.BundleID != "" {
perfOptions = append(perfOptions,
gidevice.WithPerfBundleID(dev.PerfOptions.BundleID))
}
if dev.PerfOptions.Pid != 0 {
perfOptions = append(perfOptions,
gidevice.WithPerfPID(dev.PerfOptions.Pid))
}
// config
if dev.PerfOptions.OutputInterval != 0 {
perfOptions = append(perfOptions,
gidevice.WithPerfOutputInterval(dev.PerfOptions.OutputInterval))
}
if dev.PerfOptions.SystemAttributes != nil {
perfOptions = append(perfOptions,
gidevice.WithPerfSystemAttributes(dev.PerfOptions.SystemAttributes...))
}
if dev.PerfOptions.ProcessAttributes != nil {
perfOptions = append(perfOptions,
gidevice.WithPerfProcessAttributes(dev.PerfOptions.ProcessAttributes...))
}
return
}
func (dev *IOSDevice) pcapOpitons() (pcapOptions []gidevice.PcapOption) {
if dev.PcapOptions == nil {
return
}
if dev.PcapOptions.All {
pcapOptions = append(pcapOptions, gidevice.WithPcapAll(true))
}
if dev.PcapOptions.Pid > 0 {
pcapOptions = append(pcapOptions, gidevice.WithPcapPID(dev.PcapOptions.Pid))
}
if dev.PcapOptions.ProcName != "" {
pcapOptions = append(pcapOptions, gidevice.WithPcapProcName(dev.PcapOptions.ProcName))
}
if dev.PcapOptions.BundleID != "" {
pcapOptions = append(pcapOptions, gidevice.WithPcapBundleID(dev.PcapOptions.BundleID))
}
return
}
// NewHTTPDriver creates new remote HTTP client, this will also start a new session.
func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver IWebDriver, err error) {
var localPort int
localPort, err = strconv.Atoi(os.Getenv("WDA_LOCAL_PORT"))
if err != nil {
localPort, err = builtin.GetFreePort()
if err != nil {
return nil, errors.Wrap(code.DeviceHTTPDriverError,
fmt.Sprintf("get free port failed: %v", err))
}
if err = dev.forward(localPort, dev.Port); err != nil {
return nil, errors.Wrap(code.DeviceHTTPDriverError,
fmt.Sprintf("forward tcp port failed: %v", err))
}
} else {
log.Info().Int("WDA_LOCAL_PORT", localPort).Msg("reuse WDA local port")
}
var localMjpegPort int
localMjpegPort, err = strconv.Atoi(os.Getenv("WDA_LOCAL_MJPEG_PORT"))
if err != nil {
localMjpegPort, err = builtin.GetFreePort()
if err != nil {
return nil, errors.Wrap(code.DeviceHTTPDriverError,
fmt.Sprintf("get free port failed: %v", err))
}
if err = dev.forward(localMjpegPort, dev.MjpegPort); err != nil {
return nil, errors.Wrap(code.DeviceHTTPDriverError,
fmt.Sprintf("forward tcp port failed: %v", err))
}
} else {
log.Info().Int("WDA_LOCAL_MJPEG_PORT", localMjpegPort).
Msg("reuse WDA local mjpeg port")
}
log.Info().Interface("capabilities", capabilities).
Int("localPort", localPort).Int("localMjpegPort", localMjpegPort).
Msg("init WDA HTTP driver")
wd := new(wdaDriver)
wd.client = http.DefaultClient
host := "localhost"
if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, localPort)); err != nil {
return nil, errors.Wrap(code.DeviceHTTPDriverError, err.Error())
}
if _, err = wd.NewSession(capabilities); err != nil {
return nil, errors.Wrap(code.DeviceHTTPDriverError, err.Error())
}
if wd.mjpegHTTPConn, err = net.Dial(
"tcp",
fmt.Sprintf("%s:%d", host, localMjpegPort),
); err != nil {
return nil, errors.Wrap(code.DeviceHTTPDriverError, err.Error())
}
wd.mjpegClient = convertToHTTPClient(wd.mjpegHTTPConn)
// init WDA scale
if wd.scale, err = wd.Scale(); err != nil {
return nil, err
}
return wd, nil
}
// NewUSBDriver creates new client via USB connected device, this will also start a new session.
func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver IWebDriver, err error) {
log.Info().Interface("capabilities", capabilities).
Str("udid", dev.UDID).Msg("init WDA USB driver")
wd := new(wdaDriver)
if wd.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil {
return nil, errors.Wrap(code.DeviceUSBDriverError,
fmt.Sprintf("connect port %d failed: %v", dev.Port, err))
}
wd.client = convertToHTTPClient(wd.defaultConn.RawConn())
if wd.mjpegUSBConn, err = dev.d.NewConnect(dev.MjpegPort, 0); err != nil {
return nil, errors.Wrap(code.DeviceUSBDriverError,
fmt.Sprintf("connect MJPEG port %d failed: %v", dev.MjpegPort, err))
}
wd.mjpegClient = convertToHTTPClient(wd.mjpegUSBConn.RawConn())
if wd.urlPrefix, err = url.Parse("http://" + dev.UDID); err != nil {
return nil, errors.Wrap(code.DeviceUSBDriverError, err.Error())
}
if _, err = wd.NewSession(capabilities); err != nil {
return nil, errors.Wrap(code.DeviceUSBDriverError, err.Error())
}
// init WDA scale
if wd.scale, err = wd.Scale(); err != nil {
return nil, err
}
return wd, nil
}
func (dev *IOSDevice) RunXCTest(bundleID string) (cancel context.CancelFunc, err error) {
log.Info().Str("bundleID", bundleID).Msg("run xctest")
out, cancel, err := dev.d.XCTest(bundleID)
if err != nil {
return nil, errors.Wrap(err, "run xctest failed")
}
// wait for xctest to start
time.Sleep(5 * time.Second)
f, err := os.OpenFile(fmt.Sprintf("xctest_%s.log", dev.UDID),
os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666)
if err != nil {
return nil, err
}
defer builtinLog.SetOutput(f)
// print xctest running logs
go func() {
for s := range out {
builtinLog.Print(s)
}
f.Close()
}()
return cancel, nil
}
type Application struct {
CFBundleVersion string `json:"version"`
CFBundleDisplayName string `json:"name"`
CFBundleIdentifier string `json:"bundleId"`
}
func (dev *IOSDevice) GetPackageInfo(packageName string) (AppInfo, error) {
appInfo := AppInfo{
Name: packageName,
}
applicationType := gidevice.ApplicationTypeAny
result, err := dev.d.InstallationProxyBrowse(
gidevice.WithApplicationType(applicationType),
gidevice.WithReturnAttributes("CFBundleVersion", "CFBundleDisplayName", "CFBundleIdentifier"))
if err != nil {
return appInfo, errors.Wrap(code.DeviceShellExecError,
fmt.Sprintf("get app list failed %v", err))
}
for _, app := range result {
a := Application{}
mapstructure.Decode(app, &a)
if a.CFBundleIdentifier != packageName {
continue
}
appInfo.AppBaseInfo = AppBaseInfo{
BundleId: a.CFBundleIdentifier,
VersionName: a.CFBundleVersion,
AppName: a.CFBundleDisplayName,
}
log.Info().Interface("appInfo", appInfo).Msg("get app info")
return appInfo, nil
}
return appInfo, errors.New("failed to get package version")
}