refactor: move uixt from hrp internal to pkg

This commit is contained in:
debugtalk
2022-10-10 21:50:07 +08:00
parent 1cc2d0d27f
commit 63e18ddeba
44 changed files with 54 additions and 21 deletions

560
hrp/pkg/uixt/ios_device.go Normal file
View File

@@ -0,0 +1,560 @@
package uixt
import (
"bytes"
"encoding/base64"
builtinJSON "encoding/json"
"fmt"
"io"
"mime"
"mime/multipart"
"net"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"time"
giDevice "github.com/electricbubble/gidevice"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
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 {'不允许','暂不'}`]"
)
const (
defaultWDAPort = 8100
defaultMjpegPort = 9100
)
func InitWDAClient(device *IOSDevice) (*DriverExt, error) {
// init wda device
iosDevice, err := NewIOSDevice(device.opitons()...)
if err != nil {
return nil, err
}
// init WDA driver
capabilities := NewCapabilities()
capabilities.WithDefaultAlertAction(AlertActionAccept)
var driver WebDriver
if os.Getenv("WDA_USB_DRIVER") == "" {
// default use http driver
driver, err = iosDevice.NewHTTPDriver(capabilities)
} else {
driver, err = iosDevice.NewUSBDriver(capabilities)
}
if err != nil {
return nil, errors.Wrap(err, "failed to init WDA driver")
}
// switch to iOS springboard before init WDA session
// aviod getting stuck when some super app is activate such as douyin or wexin
log.Info().Msg("go back to home screen")
if err = driver.Homescreen(); err != nil {
return nil, errors.Wrap(err, "failed to go back to home screen")
}
driverExt, err := Extend(driver)
if err != nil {
return nil, errors.Wrap(err, "failed to extend WebDriver")
}
settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{
"snapshotMaxDepth": snapshotMaxDepth,
"acceptAlertButtonSelector": 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 device.LogOn {
err = driverExt.Driver.StartCaptureLog("hrp_wda_log")
if err != nil {
return nil, err
}
}
if device.PerfOptions != nil {
data, err := iosDevice.d.PerfStart(device.perfOpitons()...)
if err != nil {
return nil, err
}
driverExt.perfStop = make(chan struct{})
// start performance monitor
go func() {
for {
select {
case <-driverExt.perfStop:
iosDevice.d.PerfStop()
return
case d := <-data:
fmt.Println(string(d))
driverExt.perfData = append(driverExt.perfData, string(d))
}
}
}()
}
driverExt.UUID = iosDevice.UUID()
return driverExt, nil
}
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 WithLogOn(logOn bool) IOSDeviceOption {
return func(device *IOSDevice) {
device.LogOn = logOn
}
}
func WithPerfOptions(options ...giDevice.PerfOption) IOSDeviceOption {
return func(device *IOSDevice) {
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 {
return nil, fmt.Errorf("init usbmux failed: %v", err)
}
if devices, err = usbmux.Devices(); err != nil {
return nil, fmt.Errorf("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
}
deviceList = append(deviceList, d)
}
}
return deviceList, nil
}
func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) {
device = &IOSDevice{
Port: defaultWDAPort,
MjpegPort: defaultMjpegPort,
}
for _, option := range options {
option(device)
}
deviceList, err := IOSDevices(device.UDID)
if err != nil {
return nil, err
}
if len(deviceList) > 0 {
device.UDID = deviceList[0].Properties().SerialNumber
log.Info().Str("udid", device.UDID).Msg("select device")
device.d = deviceList[0]
return device, nil
}
return nil, fmt.Errorf("device %s not found", device.UDID)
}
type IOSDevice struct {
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
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
}
func (dev *IOSDevice) UUID() string {
return 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 device failed")
os.Exit(1)
}
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")
}
}(lConn, rConn)
go func(lConn, rConn net.Conn) {
if _, err := io.Copy(rConn, lConn); err != nil {
log.Error().Err(err).Msg("copy local <- remote")
}
}(lConn, rConn)
}(accept)
}
}(listener, dev.d)
return nil
}
func (dev *IOSDevice) opitons() (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))
}
return
}
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
}
// NewHTTPDriver creates new remote HTTP client, this will also start a new session.
func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) {
localPort, err := getFreePort()
if err != nil {
return nil, errors.Wrap(err, "get free port failed")
}
if err = dev.forward(localPort, dev.Port); err != nil {
return nil, errors.Wrap(err, "forward tcp port failed")
}
localMjpegPort, err := getFreePort()
if err != nil {
return nil, errors.Wrap(err, "get free port failed")
}
if err = dev.forward(localMjpegPort, dev.MjpegPort); err != nil {
return nil, errors.Wrap(err, "forward tcp port failed")
}
log.Info().Interface("capabilities", capabilities).
Int("localPort", localPort).Int("localMjpegPort", localMjpegPort).
Msg("init WDA HTTP driver")
wd := new(wdaDriver)
wd.client = http.DefaultClient
host := "127.0.0.1"
if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, localPort)); err != nil {
return nil, err
}
var sessionInfo SessionInfo
if sessionInfo, err = wd.NewSession(capabilities); err != nil {
return nil, err
}
wd.sessionId = sessionInfo.SessionId
if wd.mjpegHTTPConn, err = net.Dial(
"tcp",
fmt.Sprintf("%s:%d", host, localMjpegPort),
); err != nil {
return nil, err
}
wd.mjpegClient = convertToHTTPClient(wd.mjpegHTTPConn)
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 WebDriver, 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, fmt.Errorf("connect port %d failed: %w",
dev.Port, err)
}
wd.client = convertToHTTPClient(wd.defaultConn.RawConn())
if wd.mjpegUSBConn, err = dev.d.NewConnect(dev.MjpegPort, 0); err != nil {
return nil, fmt.Errorf("connect MJPEG port %d failed: %w",
dev.MjpegPort, err)
}
wd.mjpegClient = convertToHTTPClient(wd.mjpegUSBConn.RawConn())
if wd.urlPrefix, err = url.Parse("http://" + dev.UDID); err != nil {
return nil, err
}
_, err = wd.NewSession(capabilities)
return wd, err
}
func (dExt *DriverExt) ConnectMjpegStream(httpClient *http.Client) (err error) {
if httpClient == nil {
return errors.New(`'httpClient' can't be nil`)
}
var req *http.Request
if req, err = http.NewRequest(http.MethodGet, "http://*", nil); err != nil {
return err
}
var resp *http.Response
if resp, err = httpClient.Do(req); err != nil {
return err
}
// defer func() { _ = resp.Body.Close() }()
var boundary string
if _, param, err := mime.ParseMediaType(resp.Header.Get("Content-Type")); err != nil {
return err
} else {
boundary = strings.Trim(param["boundary"], "-")
}
mjpegReader := multipart.NewReader(resp.Body, boundary)
go func() {
for {
select {
case <-dExt.doneMjpegStream:
_ = resp.Body.Close()
return
default:
var part *multipart.Part
if part, err = mjpegReader.NextPart(); err != nil {
dExt.frame = nil
continue
}
raw := new(bytes.Buffer)
if _, err = raw.ReadFrom(part); err != nil {
dExt.frame = nil
continue
}
dExt.frame = raw
}
}
}()
return
}
func (dExt *DriverExt) CloseMjpegStream() {
dExt.doneMjpegStream <- true
}
type rawResponse []byte
func (r rawResponse) checkErr() (err error) {
reply := new(struct {
Value struct {
Err string `json:"error"`
Message string `json:"message"`
Traceback string `json:"traceback"` // wda
Stacktrace string `json:"stacktrace"` // uia
}
})
if err = json.Unmarshal(r, reply); err != nil {
return err
}
if reply.Value.Err != "" {
errText := reply.Value.Message
re := regexp.MustCompile(`{.+?=(.+?)}`)
if re.MatchString(reply.Value.Message) {
subMatch := re.FindStringSubmatch(reply.Value.Message)
errText = subMatch[len(subMatch)-1]
}
return fmt.Errorf("%s: %s", reply.Value.Err, errText)
}
return
}
func (r rawResponse) valueConvertToString() (s string, err error) {
reply := new(struct{ Value string })
if err = json.Unmarshal(r, reply); err != nil {
return "", errors.Wrapf(err, "json.Unmarshal failed, rawResponse: %s", string(r))
}
s = reply.Value
return
}
func (r rawResponse) valueConvertToBool() (b bool, err error) {
reply := new(struct{ Value bool })
if err = json.Unmarshal(r, reply); err != nil {
return false, err
}
b = reply.Value
return
}
func (r rawResponse) valueConvertToSessionInfo() (sessionInfo SessionInfo, err error) {
reply := new(struct{ Value struct{ SessionInfo } })
if err = json.Unmarshal(r, reply); err != nil {
return SessionInfo{}, err
}
sessionInfo = reply.Value.SessionInfo
return
}
func (r rawResponse) valueConvertToJsonRawMessage() (raw builtinJSON.RawMessage, err error) {
reply := new(struct{ Value builtinJSON.RawMessage })
if err = json.Unmarshal(r, reply); err != nil {
return nil, err
}
raw = reply.Value
return
}
func (r rawResponse) valueDecodeAsBase64() (raw *bytes.Buffer, err error) {
str, err := r.valueConvertToString()
if err != nil {
return nil, errors.Wrap(err, "failed to convert value to string")
}
decodeString, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return nil, errors.Wrap(err, "failed to decode base64 string")
}
raw = bytes.NewBuffer(decodeString)
return
}
var errNoSuchElement = errors.New("no such element")
func (r rawResponse) valueConvertToElementID() (id string, err error) {
reply := new(struct{ Value map[string]string })
if err = json.Unmarshal(r, reply); err != nil {
return "", err
}
if len(reply.Value) == 0 {
return "", errNoSuchElement
}
if id = elementIDFromValue(reply.Value); id == "" {
return "", fmt.Errorf("invalid element returned: %+v", reply)
}
return
}
func (r rawResponse) valueConvertToElementIDs() (IDs []string, err error) {
reply := new(struct{ Value []map[string]string })
if err = json.Unmarshal(r, reply); err != nil {
return nil, err
}
if len(reply.Value) == 0 {
return nil, errNoSuchElement
}
IDs = make([]string, len(reply.Value))
for i, elem := range reply.Value {
var id string
if id = elementIDFromValue(elem); id == "" {
return nil, fmt.Errorf("invalid element returned: %+v", reply)
}
IDs[i] = id
}
return
}