From 1e1f8d215dd38a0e4168e3320995dafe7b720ab4 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Wed, 5 Mar 2025 21:40:47 +0800 Subject: [PATCH] move ghdc to pkg --- .gitignore | 1 - internal/version/VERSION | 2 +- pkg/ghdc | 1 - pkg/ghdc/README.md | 86 +++ pkg/ghdc/agent.so | Bin 0 -> 149685 bytes pkg/ghdc/client.go | 166 ++++++ pkg/ghdc/client_test.go | 103 ++++ pkg/ghdc/connection_pool.go | 76 +++ pkg/ghdc/device.go | 618 +++++++++++++++++++++ pkg/ghdc/device_test.go | 253 +++++++++ pkg/ghdc/ghdc.go | 17 + pkg/ghdc/minUiTestVersion.txt | 1 + pkg/ghdc/transport.go | 236 ++++++++ pkg/ghdc/ui_driver.go | 927 +++++++++++++++++++++++++++++++ pkg/ghdc/ui_driver_test.go | 274 +++++++++ pkg/ghdc/ui_gesture.go | 83 +++ pkg/ghdc/uitest_kit_transport.go | 345 ++++++++++++ pkg/ghdc/uitest_transport.go | 82 +++ 18 files changed, 3268 insertions(+), 3 deletions(-) delete mode 160000 pkg/ghdc create mode 100644 pkg/ghdc/README.md create mode 100644 pkg/ghdc/agent.so create mode 100644 pkg/ghdc/client.go create mode 100644 pkg/ghdc/client_test.go create mode 100644 pkg/ghdc/connection_pool.go create mode 100644 pkg/ghdc/device.go create mode 100644 pkg/ghdc/device_test.go create mode 100644 pkg/ghdc/ghdc.go create mode 100644 pkg/ghdc/minUiTestVersion.txt create mode 100644 pkg/ghdc/transport.go create mode 100644 pkg/ghdc/ui_driver.go create mode 100644 pkg/ghdc/ui_driver_test.go create mode 100644 pkg/ghdc/ui_gesture.go create mode 100644 pkg/ghdc/uitest_kit_transport.go create mode 100644 pkg/ghdc/uitest_transport.go diff --git a/.gitignore b/.gitignore index 304e1bd9..0e06d069 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ *.exe *.exe~ *.dll -*.so *.dylib *.apk diff --git a/internal/version/VERSION b/internal/version/VERSION index 9d01e724..8b8fe961 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2503052133 +v5.0.0-beta-2503052140 diff --git a/pkg/ghdc b/pkg/ghdc deleted file mode 160000 index ecb76cf5..00000000 --- a/pkg/ghdc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ecb76cf5bd27ebdfe0ed6d9810edc180692f0e39 diff --git a/pkg/ghdc/README.md b/pkg/ghdc/README.md new file mode 100644 index 00000000..ceba9649 --- /dev/null +++ b/pkg/ghdc/README.md @@ -0,0 +1,86 @@ +# ghdc + +ghdc 是一个用于与鸿蒙设备进行交互的工具,封装了各种 HDC(鸿蒙的 ADB)命令和 UI 自动化能力。 + +## 目录结构 +ghdc \ +├── client.go       封装 hdc list targets 等非设备命令 \ +├── device.go     封装 hdc -t connectkey shell 等指定设备的命令 \ +└── uidevice.go 封装设备所有自动化能力 + +## hdc 命令调用 +目前支持的能力: +- 获取设备 +- 文件传输 +- shell 命令 +- 端口挂载 +- 属性获取(brand, model, 版本等) +- 截图 + +`hdc`与鸿蒙的关系和`adb`与安卓关系一致,其架构与 adb server 相同,分为 client 和 server。`hdc start` 会启动一个 hdc server,监听本地的 8710 端口。我们也是和该 8710 端口通信执行 HDC 命令。目前支持常用的 hdc 能力,并可扩展至所有 hdc 能力。 + +```shell +hdc -m -s ::ffff:127.0.0.1:8710 +``` +hdc 命令分为两类: +- 不指定设备的命令:例如 `hdc list targets`。逻辑封装在 client.go 中。与 gadb 类似,所有的命令通过 RPC 方式与 hdc server 直接通信。 +- 指定设备的命令:例如 `hdc -t connectkey shell`。逻辑封装在 device.go 中。这些命令会在与 server 建连时指定执行的设备。 + +## UI Test 自动化能力 +目前支持的能力: +- 点击\滑动\输入 +- 按键操作 +- 手势操作 +- TouchDown/TouchMove/TouchUp 屏幕操作 +- 屏幕旋转 +- 音量设置 +- 图片流获取 +- 控件信息监听 +- 简单控件操作 + +Harmony Next 内置了 UI Test 服务,提供了所有常用的自动化能力。并且这个服务也部分开源,支持二次开发。由于协议未开源,我们通过逆向工程绕过 JS API,直接通过 socket 与 UI Test 服务进行通信,操作手机。代码能力封装在 uidevice.go 中。 + +UI Test 协议分为两类: +- 无 session 单次返回长连接:例如点击、滑动等,发出命令即可,返回一个执行成功或失败。 +- 有 session 多次返回长连接:例如获取屏幕图片流、监听控件信息变化等。 + +## 使用方法 +```go +package main + +import ( + "github.com/httprunner/httprunner/v5/pkg/ghdc" + "log" +) + +func main() { + client, err := ghdc.NewClient() + checkErr(err, "fail to connect hdc server") + + devices, err := client.DeviceList() + checkErr(err) + + if len(devices) == 0 { + log.Fatalln("list of devices is empty") + } + dev := devices[0] + driver, err := ghdc.NewUIDriver(dev) + checkErr(err, "fail to init device uiDriver") + + err = driver.Touch(225, 1715) + checkErr(err) +} + +func checkErr(err error, msg ...string) { + if err == nil { + return + } + + var output string + if len(msg) != 0 { + output = msg[0] + " " + } + output += err.Error() + log.Fatalln(output) +} +``` \ No newline at end of file diff --git a/pkg/ghdc/agent.so b/pkg/ghdc/agent.so new file mode 100644 index 0000000000000000000000000000000000000000..b8d37029af2b987b1c49477d5efb36ecc570635d GIT binary patch literal 149685 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 literal 0 HcmV?d00001 diff --git a/pkg/ghdc/client.go b/pkg/ghdc/client.go new file mode 100644 index 00000000..ca2c304d --- /dev/null +++ b/pkg/ghdc/client.go @@ -0,0 +1,166 @@ +package ghdc + +import ( + "fmt" + "log" + "strings" +) + +const HdcServerPort = 8710 + +type Client struct { + host string + port int +} + +func NewClient() (Client, error) { + return NewClientWith("localhost") +} + +func NewClientWith(host string, port ...int) (hdClient Client, err error) { + if len(port) == 0 { + port = []int{HdcServerPort} + } + hdClient.host = host + hdClient.port = port[0] + + var tp transport + if tp, err = hdClient.createTransport(); err != nil { + return Client{}, err + } + defer func() { _ = tp.Close() }() + + return +} + +func (c Client) ServerVersion() (version string, err error) { + return c.executeCommand("version") +} + +func (c Client) DeviceSerialList() (serials []string, err error) { + var resp string + if resp, err = c.executeCommand("list targets"); err != nil { + return + } + + lines := strings.Split(resp, "\n") + serials = make([]string, 0, len(lines)) + + for i := range lines { + if lines[i] == "" { + continue + } + fields := strings.Fields(lines[i]) + serials = append(serials, fields[0]) + } + + return +} + +func (c Client) DeviceList() (devices []Device, err error) { + var resp string + if resp, err = c.executeCommand("list targets -v"); err != nil { + return + } + + lines := strings.Split(resp, "\n") + devices = make([]Device, 0, len(lines)) + + for i := range lines { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + + fields := strings.Fields(line) + if len(fields) < 4 || len(fields[0]) == 0 { + debugLog(fmt.Sprintf("can't parse: %s", line)) + continue + } + if fields[2] == "Offline" { + log.Printf("device [%s] Offline ", fields[0]) + continue + } + if fields[1] == "UART" { + continue + } + slickAttrs := make(map[string]string) + slickAttrs["usb"] = fields[1] + slickAttrs["device_status"] = fields[2] + device, err := NewDevice(c, fields[0], slickAttrs) + if err != nil { + return nil, err + } + devices = append(devices, device) + } + + return +} + +func (c Client) ForwardList() (deviceForward []DeviceForward, err error) { + var resp string + if resp, err = c.executeCommand("fport ls"); err != nil { + return nil, err + } + + lines := strings.Split(resp, "\n") + deviceForward = make([]DeviceForward, 0, len(lines)) + + for i := range lines { + line := strings.TrimSpace(lines[i]) + line = strings.ReplaceAll(line, "'", "") + if line == "" { + continue + } + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + deviceForward = append(deviceForward, DeviceForward{Local: fields[0], Remote: fields[1]}) + } + + return +} + +func (c Client) Connect(ip string, port ...int) (err error) { + if len(port) == 0 { + port = []int{HdcServerPort} + } + + var resp string + if resp, err = c.executeCommand(fmt.Sprintf("tconn %s:%d", ip, port[0])); err != nil { + return err + } + if !strings.HasPrefix(resp, "connected to") && !strings.HasPrefix(resp, "already connected to") { + return fmt.Errorf("hd connect: %s", resp) + } + return +} + +func (c Client) KillServer() (err error) { + var tp transport + if tp, err = c.createTransport(); err != nil { + return err + } + defer func() { _ = tp.Close() }() + + err = tp.SendCommand("kill") + return +} + +func (c Client) createTransport() (tp transport, err error) { + return newTransport(fmt.Sprintf("%s:%d", c.host, c.port), false, "") +} + +func (c Client) executeCommand(command string) (resp string, err error) { + var tp transport + if tp, err = c.createTransport(); err != nil { + return "", err + } + defer func() { _ = tp.Close() }() + + if err = tp.SendCommand(command); err != nil { + return "", err + } + return tp.ReadStringAll() +} diff --git a/pkg/ghdc/client_test.go b/pkg/ghdc/client_test.go new file mode 100644 index 00000000..397194bb --- /dev/null +++ b/pkg/ghdc/client_test.go @@ -0,0 +1,103 @@ +package ghdc + +import ( + "testing" +) + +func TestClient_ServerVersion(t *testing.T) { + SetDebug(true) + + hdcClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + hdServerVersion, err := hdcClient.ServerVersion() + if err != nil { + t.Fatal(err) + } + + t.Log(hdServerVersion) +} + +func TestClient_DeviceSerialList(t *testing.T) { + SetDebug(true) + + hdcClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + serials, err := hdcClient.DeviceSerialList() + if err != nil { + t.Fatal(err) + } + + for i := range serials { + t.Log(serials[i]) + } +} + +func TestClient_DeviceList(t *testing.T) { + SetDebug(true) + + hdcClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := hdcClient.DeviceList() + if err != nil { + t.Fatal(err) + } + + for i := range devices { + t.Log(devices[i].serial, devices[i].DeviceInfo()) + } +} + +func TestClient_ForwardList(t *testing.T) { + SetDebug(true) + + hdcClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + deviceForwardList, err := hdcClient.ForwardList() + if err != nil { + t.Fatal(err) + } + + for i := range deviceForwardList { + t.Log(deviceForwardList[i]) + } +} + +func TestClient_Connect(t *testing.T) { + hdcClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + SetDebug(true) + + err = hdcClient.Connect("192.168.1.28") + if err != nil { + t.Fatal(err) + } +} + +func TestClient_KillServer(t *testing.T) { + SetDebug(true) + + hdcClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + err = hdcClient.KillServer() + if err != nil { + t.Fatal(err) + } +} diff --git a/pkg/ghdc/connection_pool.go b/pkg/ghdc/connection_pool.go new file mode 100644 index 00000000..f8639a76 --- /dev/null +++ b/pkg/ghdc/connection_pool.go @@ -0,0 +1,76 @@ +package ghdc + +import ( + "fmt" + "net" + "sync" + "time" +) + +type ConnectionPool struct { + mu sync.Mutex + conns chan net.Conn + host string + port string + maxConns int +} + +func newConnectionPool(host string, port string, maxConns int) (*ConnectionPool, error) { + pool := &ConnectionPool{ + conns: make(chan net.Conn, maxConns), + host: host, + port: port, + maxConns: maxConns, + } + + for i := 0; i < maxConns; i++ { + conn, err := pool.newConnection() + if err != nil { + return nil, err + } + pool.conns <- conn + } + + return pool, nil +} + +func (p *ConnectionPool) newConnection() (net.Conn, error) { + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", p.host, p.port), 5*time.Second) + if err != nil { + return nil, err + } + return conn, nil +} + +func (p *ConnectionPool) getConnection() (net.Conn, error) { + select { + case conn := <-p.conns: + return conn, nil + default: + // 如果池中没有可用连接,创建一个新的连接 + return p.newConnection() + } +} + +func (p *ConnectionPool) releaseConnection(conn net.Conn) { + p.mu.Lock() + defer p.mu.Unlock() + + select { + case p.conns <- conn: + // 放回连接池成功 + default: + // 连接池已满,关闭连接 + conn.Close() + } +} + +func (p *ConnectionPool) close() { + p.mu.Lock() + defer p.mu.Unlock() + + close(p.conns) + for conn := range p.conns { + conn.Close() + } +} diff --git a/pkg/ghdc/device.go b/pkg/ghdc/device.go new file mode 100644 index 00000000..e4a635be --- /dev/null +++ b/pkg/ghdc/device.go @@ -0,0 +1,618 @@ +package ghdc + +import ( + "errors" + "fmt" + "net" + "os" + "strings" + "time" +) + +type DeviceFileInfo struct { + Name string + Mode os.FileMode + Size uint32 + LastModified time.Time +} + +func (info DeviceFileInfo) IsDir() bool { + return (info.Mode & (1 << 14)) == (1 << 14) +} + +type DeviceState string + +const ( + StateOnline DeviceState = "Online" + StateOffline DeviceState = "Offline" +) + +var deviceStateStrings = map[string]DeviceState{ + "Offline": StateOffline, + "Connected": StateOnline, +} + +type KeyCode int + +const ( + KEYCODE_FN KeyCode = 0 + KEYCODE_UNKNOWN KeyCode = -1 + KEYCODE_HOME KeyCode = 1 + KEYCODE_BACK KeyCode = 2 + KEYCODE_MEDIA_PLAY_PAUSE KeyCode = 10 + KEYCODE_MEDIA_STOP KeyCode = 11 + KEYCODE_MEDIA_NEXT KeyCode = 12 + KEYCODE_MEDIA_PREVIOUS KeyCode = 13 + KEYCODE_MEDIA_REWIND KeyCode = 14 + KEYCODE_MEDIA_FAST_FORWARD KeyCode = 15 + KEYCODE_VOLUME_UP KeyCode = 16 + KEYCODE_VOLUME_DOWN KeyCode = 17 + KEYCODE_POWER KeyCode = 18 + KEYCODE_CAMERA KeyCode = 19 + KEYCODE_VOLUME_MUTE KeyCode = 22 + KEYCODE_MUTE KeyCode = 23 + KEYCODE_BRIGHTNESS_UP KeyCode = 40 + KEYCODE_BRIGHTNESS_DOWN KeyCode = 41 + KEYCODE_NUM_0 KeyCode = 2000 + KEYCODE_NUM_1 KeyCode = 2001 + KEYCODE_NUM_2 KeyCode = 2002 + KEYCODE_NUM_3 KeyCode = 2003 + KEYCODE_NUM_4 KeyCode = 2004 + KEYCODE_NUM_5 KeyCode = 2005 + KEYCODE_NUM_6 KeyCode = 2006 + KEYCODE_NUM_7 KeyCode = 2007 + KEYCODE_NUM_8 KeyCode = 2008 + KEYCODE_NUM_9 KeyCode = 2009 + KEYCODE_STAR KeyCode = 2010 + KEYCODE_POUND KeyCode = 2011 + KEYCODE_DPAD_UP KeyCode = 2012 + KEYCODE_DPAD_DOWN KeyCode = 2013 + KEYCODE_DPAD_LEFT KeyCode = 2014 + KEYCODE_DPAD_RIGHT KeyCode = 2015 + KEYCODE_DPAD_CENTER KeyCode = 2016 + KEYCODE_A KeyCode = 2017 + KEYCODE_B KeyCode = 2018 + KEYCODE_C KeyCode = 2019 + KEYCODE_D KeyCode = 2020 + KEYCODE_E KeyCode = 2021 + KEYCODE_F KeyCode = 2022 + KEYCODE_G KeyCode = 2023 + KEYCODE_H KeyCode = 2024 + KEYCODE_I KeyCode = 2025 + KEYCODE_J KeyCode = 2026 + KEYCODE_K KeyCode = 2027 + KEYCODE_L KeyCode = 2028 + KEYCODE_M KeyCode = 2029 + KEYCODE_N KeyCode = 2030 + KEYCODE_O KeyCode = 2031 + KEYCODE_P KeyCode = 2032 + KEYCODE_Q KeyCode = 2033 + KEYCODE_R KeyCode = 2034 + KEYCODE_S KeyCode = 2035 + KEYCODE_T KeyCode = 2036 + KEYCODE_U KeyCode = 2037 + KEYCODE_V KeyCode = 2038 + KEYCODE_W KeyCode = 2039 + KEYCODE_X KeyCode = 2040 + KEYCODE_Y KeyCode = 2041 + KEYCODE_Z KeyCode = 2042 + KEYCODE_COMMA KeyCode = 2043 + KEYCODE_PERIOD KeyCode = 2044 + KEYCODE_ALT_LEFT KeyCode = 2045 + KEYCODE_ALT_RIGHT KeyCode = 2046 + KEYCODE_SHIFT_LEFT KeyCode = 2047 + KEYCODE_SHIFT_RIGHT KeyCode = 2048 + KEYCODE_TAB KeyCode = 2049 + KEYCODE_SPACE KeyCode = 2050 + KEYCODE_SYM KeyCode = 2051 + KEYCODE_EXPLORER KeyCode = 2052 + KEYCODE_ENVELOPE KeyCode = 2053 + KEYCODE_ENTER KeyCode = 2054 + KEYCODE_DEL KeyCode = 2055 + KEYCODE_GRAVE KeyCode = 2056 + KEYCODE_MINUS KeyCode = 2057 + KEYCODE_EQUALS KeyCode = 2058 + KEYCODE_LEFT_BRACKET KeyCode = 2059 + KEYCODE_RIGHT_BRACKET KeyCode = 2060 + KEYCODE_BACKSLASH KeyCode = 2061 + KEYCODE_SEMICOLON KeyCode = 2062 + KEYCODE_APOSTROPHE KeyCode = 2063 + KEYCODE_SLASH KeyCode = 2064 + KEYCODE_AT KeyCode = 2065 + KEYCODE_PLUS KeyCode = 2066 + KEYCODE_MENU KeyCode = 2067 + KEYCODE_PAGE_UP KeyCode = 2068 + KEYCODE_PAGE_DOWN KeyCode = 2069 + KEYCODE_ESCAPE KeyCode = 2070 + KEYCODE_FORWARD_DEL KeyCode = 2071 + KEYCODE_CTRL_LEFT KeyCode = 2072 + KEYCODE_CTRL_RIGHT KeyCode = 2073 + KEYCODE_CAPS_LOCK KeyCode = 2074 + KEYCODE_SCROLL_LOCK KeyCode = 2075 + KEYCODE_META_LEFT KeyCode = 2076 + KEYCODE_META_RIGHT KeyCode = 2077 + KEYCODE_FUNCTION KeyCode = 2078 + KEYCODE_SYSRQ KeyCode = 2079 + KEYCODE_BREAK KeyCode = 2080 + KEYCODE_MOVE_HOME KeyCode = 2081 + KEYCODE_MOVE_END KeyCode = 2082 + KEYCODE_INSERT KeyCode = 2083 + KEYCODE_FORWARD KeyCode = 2084 + KEYCODE_MEDIA_PLAY KeyCode = 2085 + KEYCODE_MEDIA_PAUSE KeyCode = 2086 + KEYCODE_MEDIA_CLOSE KeyCode = 2087 + KEYCODE_MEDIA_EJECT KeyCode = 2088 + KEYCODE_MEDIA_RECORD KeyCode = 2089 + KEYCODE_F1 KeyCode = 2090 + KEYCODE_F2 KeyCode = 2091 + KEYCODE_F3 KeyCode = 2092 + KEYCODE_F4 KeyCode = 2093 + KEYCODE_F5 KeyCode = 2094 + KEYCODE_F6 KeyCode = 2095 + KEYCODE_F7 KeyCode = 2096 + KEYCODE_F8 KeyCode = 2097 + KEYCODE_F9 KeyCode = 2098 + KEYCODE_F10 KeyCode = 2099 + KEYCODE_F11 KeyCode = 2100 + KEYCODE_F12 KeyCode = 2101 + KEYCODE_NUM_LOCK KeyCode = 2102 + KEYCODE_NUMPAD_0 KeyCode = 2103 + KEYCODE_NUMPAD_1 KeyCode = 2104 + KEYCODE_NUMPAD_2 KeyCode = 2105 + KEYCODE_NUMPAD_3 KeyCode = 2106 + KEYCODE_NUMPAD_4 KeyCode = 2107 + KEYCODE_NUMPAD_5 KeyCode = 2108 + KEYCODE_NUMPAD_6 KeyCode = 2109 + KEYCODE_NUMPAD_7 KeyCode = 2110 + KEYCODE_NUMPAD_8 KeyCode = 2111 + KEYCODE_NUMPAD_9 KeyCode = 2112 + KEYCODE_NUMPAD_DIVIDE KeyCode = 2113 + KEYCODE_NUMPAD_MULTIPLY KeyCode = 2114 + KEYCODE_NUMPAD_SUBTRACT KeyCode = 2115 + KEYCODE_NUMPAD_ADD KeyCode = 2116 + KEYCODE_NUMPAD_DOT KeyCode = 2117 + KEYCODE_NUMPAD_COMMA KeyCode = 2118 + KEYCODE_NUMPAD_ENTER KeyCode = 2119 + KEYCODE_NUMPAD_EQUALS KeyCode = 2120 + KEYCODE_NUMPAD_LEFT_PAREN KeyCode = 2121 + KEYCODE_NUMPAD_RIGHT_PAREN KeyCode = 2122 + KEYCODE_VIRTUAL_MULTITASK KeyCode = 2210 + KEYCODE_SLEEP KeyCode = 2600 + KEYCODE_ZENKAKU_HANKAKU KeyCode = 2601 + KEYCODE_ND KeyCode = 2602 + KEYCODE_RO KeyCode = 2603 + KEYCODE_KATAKANA KeyCode = 2604 + KEYCODE_HIRAGANA KeyCode = 2605 + KEYCODE_HENKAN KeyCode = 2606 + KEYCODE_KATAKANA_HIRAGANA KeyCode = 2607 + KEYCODE_MUHENKAN KeyCode = 2608 + KEYCODE_LINEFEED KeyCode = 2609 + KEYCODE_MACRO KeyCode = 2610 + KEYCODE_NUMPAD_PLUSMINUS KeyCode = 2611 + KEYCODE_SCALE KeyCode = 2612 + KEYCODE_HANGUEL KeyCode = 2613 + KEYCODE_HANJA KeyCode = 2614 + KEYCODE_YEN KeyCode = 2615 + KEYCODE_STOP KeyCode = 2616 + KEYCODE_AGAIN KeyCode = 2617 + KEYCODE_PROPS KeyCode = 2618 + KEYCODE_UNDO KeyCode = 2619 + KEYCODE_COPY KeyCode = 2620 + KEYCODE_OPEN KeyCode = 2621 + KEYCODE_PASTE KeyCode = 2622 + KEYCODE_FIND KeyCode = 2623 + KEYCODE_CUT KeyCode = 2624 + KEYCODE_HELP KeyCode = 2625 + KEYCODE_CALC KeyCode = 2626 + KEYCODE_FILE KeyCode = 2627 + KEYCODE_BOOKMARKS KeyCode = 2628 + KEYCODE_NEXT KeyCode = 2629 + KEYCODE_PLAYPAUSE KeyCode = 2630 + KEYCODE_PREVIOUS KeyCode = 2631 + KEYCODE_STOPCD KeyCode = 2632 + KEYCODE_CONFIG KeyCode = 2634 + KEYCODE_REFRESH KeyCode = 2635 + KEYCODE_EXIT KeyCode = 2636 + KEYCODE_EDIT KeyCode = 2637 + KEYCODE_SCROLLUP KeyCode = 2638 + KEYCODE_SCROLLDOWN KeyCode = 2639 + KEYCODE_NEW KeyCode = 2640 + KEYCODE_REDO KeyCode = 2641 + KEYCODE_CLOSE KeyCode = 2642 + KEYCODE_PLAY KeyCode = 2643 + KEYCODE_BASSBOOST KeyCode = 2644 + KEYCODE_PRINT KeyCode = 2645 + KEYCODE_CHAT KeyCode = 2646 + KEYCODE_FINANCE KeyCode = 2647 + KEYCODE_CANCEL KeyCode = 2648 + KEYCODE_KBDILLUM_TOGGLE KeyCode = 2649 + KEYCODE_KBDILLUM_DOWN KeyCode = 2650 + KEYCODE_KBDILLUM_UP KeyCode = 2651 + KEYCODE_SEND KeyCode = 2652 + KEYCODE_REPLY KeyCode = 2653 + KEYCODE_FORWARDMAIL KeyCode = 2654 + KEYCODE_SAVE KeyCode = 2655 + KEYCODE_DOCUMENTS KeyCode = 2656 + KEYCODE_VIDEO_NEXT KeyCode = 2657 + KEYCODE_VIDEO_PREV KeyCode = 2658 + KEYCODE_BRIGHTNESS_CYCLE KeyCode = 2659 + KEYCODE_BRIGHTNESS_ZERO KeyCode = 2660 + KEYCODE_DISPLAY_OFF KeyCode = 2661 + KEYCODE_BTN_MISC KeyCode = 2662 + KEYCODE_GOTO KeyCode = 2663 + KEYCODE_INFO KeyCode = 2664 + KEYCODE_PROGRAM KeyCode = 2665 + KEYCODE_PVR KeyCode = 2666 + KEYCODE_SUBTITLE KeyCode = 2667 + KEYCODE_FULL_SCREEN KeyCode = 2668 + KEYCODE_KEYBOARD KeyCode = 2669 + KEYCODE_ASPECT_RATIO KeyCode = 2670 + KEYCODE_PC KeyCode = 2671 + KEYCODE_TV KeyCode = 2672 + KEYCODE_TV2 KeyCode = 2673 + KEYCODE_VCR KeyCode = 2674 + KEYCODE_VCR2 KeyCode = 2675 + KEYCODE_SAT KeyCode = 2676 + KEYCODE_CD KeyCode = 2677 + KEYCODE_TAPE KeyCode = 2678 + KEYCODE_TUNER KeyCode = 2679 + KEYCODE_PLAYER KeyCode = 2680 + KEYCODE_DVD KeyCode = 2681 + KEYCODE_AUDIO KeyCode = 2682 + KEYCODE_VIDEO KeyCode = 2683 + KEYCODE_MEMO KeyCode = 2684 + KEYCODE_CALENDAR KeyCode = 2685 + KEYCODE_RED KeyCode = 2686 + KEYCODE_GREEN KeyCode = 2687 + KEYCODE_YELLOW KeyCode = 2688 + KEYCODE_BLUE KeyCode = 2689 + KEYCODE_CHANNELUP KeyCode = 2690 + KEYCODE_CHANNELDOWN KeyCode = 2691 + KEYCODE_LAST KeyCode = 2692 + KEYCODE_RESTART KeyCode = 2693 + KEYCODE_SLOW KeyCode = 2694 + KEYCODE_SHUFFLE KeyCode = 2695 + KEYCODE_VIDEOPHONE KeyCode = 2696 + KEYCODE_GAMES KeyCode = 2697 + KEYCODE_ZOOMIN KeyCode = 2698 + KEYCODE_ZOOMOUT KeyCode = 2699 + KEYCODE_ZOOMRESET KeyCode = 2700 + KEYCODE_WORDPROCESSOR KeyCode = 2701 + KEYCODE_EDITOR KeyCode = 2702 + KEYCODE_SPREADSHEET KeyCode = 2703 + KEYCODE_GRAPHICSEDITOR KeyCode = 2704 + KEYCODE_PRESENTATION KeyCode = 2705 + KEYCODE_DATABASE KeyCode = 2706 + KEYCODE_NEWS KeyCode = 2707 + KEYCODE_VOICEMAIL KeyCode = 2708 + KEYCODE_ADDRESSBOOK KeyCode = 2709 + KEYCODE_MESSENGER KeyCode = 2710 + KEYCODE_BRIGHTNESS_TOGGLE KeyCode = 2711 + KEYCODE_SPELLCHECK KeyCode = 2712 + KEYCODE_COFFEE KeyCode = 2713 + KEYCODE_MEDIA_REPEAT KeyCode = 2714 + KEYCODE_IMAGES KeyCode = 2715 + KEYCODE_BUTTONCONFIG KeyCode = 2716 + KEYCODE_TASKMANAGER KeyCode = 2717 + KEYCODE_JOURNAL KeyCode = 2718 + KEYCODE_CONTROLPANEL KeyCode = 2719 + KEYCODE_APPSELECT KeyCode = 2720 + KEYCODE_SCREENSAVER KeyCode = 2721 + KEYCODE_ASSISTANT KeyCode = 2722 + KEYCODE_KBD_LAYOUT_NEXT KeyCode = 2723 + KEYCODE_BRIGHTNESS_MIN KeyCode = 2724 + KEYCODE_BRIGHTNESS_MAX KeyCode = 2725 + KEYCODE_KBDINPUTASSIST_PREV KeyCode = 2726 + KEYCODE_KBDINPUTASSIST_NEXT KeyCode = 2727 + KEYCODE_KBDINPUTASSIST_PREVGROUP KeyCode = 2728 + KEYCODE_KBDINPUTASSIST_NEXTGROUP KeyCode = 2729 + KEYCODE_KBDINPUTASSIST_ACCEPT KeyCode = 2730 + KEYCODE_KBDINPUTASSIST_CANCEL KeyCode = 2731 + KEYCODE_FRONT KeyCode = 2800 + KEYCODE_SETUP KeyCode = 2801 + KEYCODE_WAKE_UP KeyCode = 2802 + KEYCODE_SENDFILE KeyCode = 2803 + KEYCODE_DELETEFILE KeyCode = 2804 + KEYCODE_XFER KeyCode = 2805 + KEYCODE_PROG1 KeyCode = 2806 + KEYCODE_PROG2 KeyCode = 2807 + KEYCODE_MSDOS KeyCode = 2808 + KEYCODE_SCREENLOCK KeyCode = 2809 + KEYCODE_DIRECTION_ROTATE_DISPLAY KeyCode = 2810 + KEYCODE_CYCLEWINDOWS KeyCode = 2811 + KEYCODE_COMPUTER KeyCode = 2812 + KEYCODE_EJECTCLOSECD KeyCode = 2813 + KEYCODE_ISO KeyCode = 2814 + KEYCODE_MOVE KeyCode = 2815 + KEYCODE_F13 KeyCode = 2816 + KEYCODE_F14 KeyCode = 2817 + KEYCODE_F15 KeyCode = 2818 + KEYCODE_F16 KeyCode = 2819 + KEYCODE_F17 KeyCode = 2820 + KEYCODE_F18 KeyCode = 2821 + KEYCODE_F19 KeyCode = 2822 + KEYCODE_F20 KeyCode = 2823 + KEYCODE_F21 KeyCode = 2824 + KEYCODE_F22 KeyCode = 2825 + KEYCODE_F23 KeyCode = 2826 + KEYCODE_F24 KeyCode = 2827 + KEYCODE_PROG3 KeyCode = 2828 + KEYCODE_PROG4 KeyCode = 2829 + KEYCODE_DASHBOARD KeyCode = 2830 + KEYCODE_SUSPEND KeyCode = 2831 + KEYCODE_HP KeyCode = 2832 + KEYCODE_SOUND KeyCode = 2833 + KEYCODE_QUESTION KeyCode = 2834 + KEYCODE_CONNECT KeyCode = 2836 + KEYCODE_SPORT KeyCode = 2837 + KEYCODE_SHOP KeyCode = 2838 + KEYCODE_ALTERASE KeyCode = 2839 + KEYCODE_SWITCHVIDEOMODE KeyCode = 2841 + KEYCODE_BATTERY KeyCode = 2842 + KEYCODE_BLUETOOTH KeyCode = 2843 + KEYCODE_WLAN KeyCode = 2844 + KEYCODE_UWB KeyCode = 2845 + KEYCODE_WWAN_WIMAX KeyCode = 2846 + KEYCODE_RFKILL KeyCode = 2847 + KEYCODE_CHANNEL KeyCode = 3001 + KEYCODE_BTN_0 KeyCode = 3100 + KEYCODE_BTN_1 KeyCode = 3101 + KEYCODE_BTN_2 KeyCode = 3102 + KEYCODE_BTN_3 KeyCode = 3103 + KEYCODE_BTN_4 KeyCode = 3104 + KEYCODE_BTN_5 KeyCode = 3105 + KEYCODE_BTN_6 KeyCode = 3106 + KEYCODE_BTN_7 KeyCode = 3107 + KEYCODE_BTN_8 KeyCode = 3108 + KEYCODE_BTN_9 KeyCode = 3109 +) + +type DeviceForward struct { + Local string + Remote string +} + +type Device struct { + hdClient Client + serial string + attrs map[string]string +} + +func NewDevice(hdClient Client, serial string, attrs map[string]string) (Device, error) { + device := Device{hdClient: hdClient, serial: serial, attrs: attrs} + model, err := device.RunShellCommand("param get const.product.model") + if err != nil { + return device, err + } + attrs["model"] = model + + brand, err := device.RunShellCommand("param get const.product.brand") + if err != nil { + return device, err + } + attrs["brand"] = brand + + sdkVersion, err := device.RunShellCommand("param get const.product.software.version") + if err != nil { + return device, err + } + attrs["sdkVersion"] = sdkVersion + + osVersion, err := device.RunShellCommand("param get const.ohos.apiversion") + if err != nil { + return device, err + } + attrs["osVersion"] = osVersion + + cpu, err := device.RunShellCommand("param get const.product.cpu.abilist") + if err != nil { + return device, err + } + attrs["cpu"] = cpu + + product, err := device.RunShellCommand("param get const.product.name") + if err != nil { + return device, err + } + attrs["product"] = product + + _, err = device.RunShellCommand("setenforce 1") + if err != nil { + return device, err + } + + return device, nil +} + +func (d Device) HasAttribute(key string) bool { + _, ok := d.attrs[key] + return ok +} + +func (d Device) Product() (string, error) { + if d.HasAttribute("product") { + return d.attrs["product"], nil + } + return "", errors.New("does not have attribute: product") +} + +func (d Device) Model() (string, error) { + if d.HasAttribute("model") { + return d.attrs["model"], nil + } + return "", errors.New("does not have attribute: model") +} + +func (d Device) Usb() (string, error) { + if d.HasAttribute("usb") { + return d.attrs["usb"], nil + } + return "", errors.New("does not have attribute: usb") +} + +func (d Device) DeviceInfo() map[string]string { + return d.attrs +} + +func (d Device) Serial() string { + return d.serial +} + +func (d Device) IsUsb() (bool, error) { + usb, err := d.Usb() + if err != nil { + return false, err + } + + return usb != "", nil +} + +func (d Device) Screenshot(localPath string) error { + tmpPath := fmt.Sprintf("/data/local/tmp/hypium_tmp_shot_%d.jpeg", time.Now().Unix()) + _, err := d.RunShellCommand("snapshot_display", "-f", tmpPath) + if err != nil { + err = fmt.Errorf("failed to take screencap \n%v", err) + return err + } + err = d.PullFile(tmpPath, localPath) + if err != nil { + return err + } + _, _ = d.RunShellCommand("rm", "-rf", "tmpPath") + return nil +} + +func (d Device) Install(localPath string) error { + res, err := d.ExecuteCommand(fmt.Sprintf("install -r %s", localPath)) + if err != nil || !strings.Contains(res, "success") { + err = fmt.Errorf("failed to install %s %v, Msg: %s", localPath, err, res) + return err + } + return nil +} + +func (d Device) Forward(remotePort int) (localPort int, err error) { + remote := fmt.Sprintf("tcp:%d", remotePort) + localPort, err = GetFreePort() + if err != nil { + err = fmt.Errorf("failed to get free port \n%v", err) + return + } + + command := "" + local := fmt.Sprintf("tcp:%d", localPort) + + command = fmt.Sprintf("fport %s %s", local, remote) + _, err = d.ExecuteCommand(command) + return +} + +func (d Device) ForwardKill(localPort int) (err error) { + local := fmt.Sprintf("tcp:%d", localPort) + _, err = d.hdClient.executeCommand(fmt.Sprintf("-t %s fport rm %s", d.serial, local)) + return +} + +func (d Device) RunShellCommand(cmd string, args ...string) (string, error) { + if len(args) > 0 { + cmd = fmt.Sprintf("%s %s", cmd, strings.Join(args, " ")) + } + if strings.TrimSpace(cmd) == "" { + return "", errors.New("hd shell: command cannot be empty") + } + return d.ExecuteCommand(fmt.Sprintf("shell %s", cmd)) +} + +func (d Device) createDeviceTransport() (tp transport, err error) { + return newTransport(fmt.Sprintf("%s:%d", d.hdClient.host, d.hdClient.port), false, d.serial) +} + +func (d Device) createDeviceAliveTransport() (tp transport, err error) { + return newTransport(fmt.Sprintf("%s:%d", d.hdClient.host, d.hdClient.port), true, d.serial) +} + +func (d Device) createUitestTransport() (uTp uitestTransport, err error) { + port, err := d.Forward(8012) + if err != nil { + err = fmt.Errorf("failed to forward uitest port \n%v", err) + return + } + return newUitestTransport(d.hdClient.host, fmt.Sprintf("%d", port)) +} + +func (d Device) createUitestKitTransport() (uTp uitestKitTransport, err error) { + port, err := d.Forward(8012) + if err != nil { + err = fmt.Errorf("failed to forward uitest port \n%v", err) + return + } + return newUitestKitTransport(d.serial, d.hdClient.host, fmt.Sprintf("%d", port)) +} + +func (d Device) ExecuteCommand(command string) (resp string, err error) { + var tp transport + if tp, err = d.createDeviceTransport(); err != nil { + return "", err + } + defer func() { _ = tp.Close() }() + time.Sleep(1 * time.Millisecond) + if err = tp.SendCommand(command); err != nil { + return "", err + } + resp, err = tp.ReadStringAll() + if err != nil { + return + } + if strings.Contains(resp, "[Fail]") { + return resp, fmt.Errorf("failed to execute command 「%s」 \nerror: %s", command, resp) + } + return resp, nil +} + +func (d Device) PushFile(localPath string, remotePath string) (err error) { + var tp transport + if tp, err = d.createDeviceTransport(); err != nil { + return err + } + defer func() { _ = tp.Close() }() + if err = tp.SendCommand(fmt.Sprintf("file send %s %s", localPath, remotePath)); err != nil { + return err + } + _, err = tp.ReadAll() + return nil +} + +func (d Device) PullFile(remotePath string, localPath string) (err error) { + var tp transport + if tp, err = d.createDeviceTransport(); err != nil { + return err + } + defer func() { _ = tp.Close() }() + + if err = tp.SendCommand(fmt.Sprintf("file recv %s %s", remotePath, localPath)); err != nil { + return err + } + res, err := tp.ReadStringAll() + if err == nil { + if strings.Contains(res, "Fail") { + return fmt.Errorf("failed to pull: msg: %s", res) + } + } + return nil +} + +func GetFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, fmt.Errorf("resolve tcp addr failed \n%v", err) + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, fmt.Errorf("listen tcp addr failed \n%v", err) + } + defer func() { + _ = l.Close() + }() + return l.Addr().(*net.TCPAddr).Port, nil +} diff --git a/pkg/ghdc/device_test.go b/pkg/ghdc/device_test.go new file mode 100644 index 00000000..57fd645e --- /dev/null +++ b/pkg/ghdc/device_test.go @@ -0,0 +1,253 @@ +package ghdc + +import ( + "fmt" + "testing" +) + +func TestDevice_Product(t *testing.T) { + hdClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := hdClient.DeviceList() + if err != nil { + t.Fatal(err) + } + + for i := range devices { + dev := devices[i] + product, err := dev.Product() + if err != nil { + t.Fatal(err) + } + t.Log(dev.Serial(), product) + } +} + +func TestDevice_Model(t *testing.T) { + hdClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := hdClient.DeviceList() + if err != nil { + t.Fatal(err) + } + + for i := range devices { + dev := devices[i] + model, err := dev.Model() + if err != nil { + t.Fatal(err) + } + t.Log(dev.Serial(), model) + } +} + +func TestDevice_Usb(t *testing.T) { + hdClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := hdClient.DeviceList() + if err != nil { + t.Fatal(err) + } + + for i := range devices { + dev := devices[i] + usb, err := dev.Usb() + if err != nil { + t.Fatal(err) + } + isUsb, err := dev.IsUsb() + if err != nil { + t.Fatal(err) + } + t.Log(dev.Serial(), usb, isUsb) + } +} + +func TestDevice_DeviceInfo(t *testing.T) { + hdClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := hdClient.DeviceList() + if err != nil { + t.Fatal(err) + } + + for i := range devices { + dev := devices[i] + t.Log(dev.DeviceInfo()) + } +} + +func TestDevice_Forward(t *testing.T) { + hdClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := hdClient.DeviceList() + if err != nil { + t.Fatal(err) + } + if len(devices) == 0 { + t.Fatal("not found available device") + } + SetDebug(true) + + localPort := 61000 + localPort, err = devices[0].Forward(6790) + t.Log(fmt.Sprintf("forward local port %d \n", localPort)) + if err != nil { + t.Fatal(err) + } + + err = devices[0].ForwardKill(localPort) + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_ForwardKill(t *testing.T) { + hdClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := hdClient.DeviceList() + if err != nil { + t.Fatal(err) + } + if len(devices) == 0 { + t.Fatal("not found available device") + } + SetDebug(true) + + err = devices[0].ForwardKill(6790) + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_RunShellCommand(t *testing.T) { + hdClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := hdClient.DeviceList() + if err != nil { + t.Fatal(err) + } + if len(devices) == 0 { + t.Fatal("not found available device") + } + dev := devices[0] + + cmdOutput, err := dev.RunShellCommand("pwd") + if err != nil { + t.Fatal(dev.serial, err) + } + t.Log("\n⬇️"+dev.serial+"⬇️\n", cmdOutput) +} + +func TestDevice_Push(t *testing.T) { + hdClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := hdClient.DeviceList() + if err != nil { + t.Fatal(err) + } + if len(devices) == 0 { + t.Fatal("not found available device") + } + dev := devices[0] + + SetDebug(true) + + err = dev.PushFile("/tmp/test.txt", "/data/local/tmp/push.txt") + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_Pull(t *testing.T) { + hdClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := hdClient.DeviceList() + if err != nil { + t.Fatal(err) + } + if len(devices) == 0 { + t.Fatal("not found available device") + } + dev := devices[0] + + SetDebug(true) + + err = dev.PullFile("/data/local/tmp/push.txt", "/tmp/test2.txt") + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_Screenshot(t *testing.T) { + hdClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := hdClient.DeviceList() + if err != nil { + t.Fatal(err) + } + if len(devices) == 0 { + t.Fatal("not found available device") + } + dev := devices[0] + + SetDebug(true) + + err = dev.Screenshot("/tmp/test.jpeg") + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_GetSoVersion(t *testing.T) { + hdClient, err := NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := hdClient.DeviceList() + if err != nil { + t.Fatal(err) + } + if len(devices) == 0 { + t.Fatal("not found available device") + } + dev := devices[0] + + SetDebug(true) + res, err := dev.RunShellCommand("cat data/local/tmp/agent.so |grep -a UITEST_AGENT_LIBRARY") + if err != nil { + t.Fatal(err) + } + t.Log(res) +} diff --git a/pkg/ghdc/ghdc.go b/pkg/ghdc/ghdc.go new file mode 100644 index 00000000..307fb803 --- /dev/null +++ b/pkg/ghdc/ghdc.go @@ -0,0 +1,17 @@ +package ghdc + +import "log" + +var debugFlag = false + +// SetDebug set debug mode +func SetDebug(debug bool) { + debugFlag = debug +} + +func debugLog(msg string) { + if !debugFlag { + return + } + log.Println("[DEBUG] [ghd] " + msg) +} diff --git a/pkg/ghdc/minUiTestVersion.txt b/pkg/ghdc/minUiTestVersion.txt new file mode 100644 index 00000000..fc931ed3 --- /dev/null +++ b/pkg/ghdc/minUiTestVersion.txt @@ -0,0 +1 @@ +4.1.4.0 \ No newline at end of file diff --git a/pkg/ghdc/transport.go b/pkg/ghdc/transport.go new file mode 100644 index 00000000..1bbd5523 --- /dev/null +++ b/pkg/ghdc/transport.go @@ -0,0 +1,236 @@ +package ghdc + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "strconv" + "time" +) + +var ErrConnBroken = errors.New("socket connection broken") + +var DefaultReadTimeout time.Duration = 60 * time.Second + +var DATA_UNIT_LENGTH = 4 + +type transport struct { + sock net.Conn + readTimeout time.Duration + connectKey string +} + +func newTransport(address string, alive bool, connectKey string, readTimeout ...time.Duration) (tp transport, err error) { + if len(readTimeout) == 0 { + readTimeout = []time.Duration{DefaultReadTimeout} + } + tp.readTimeout = readTimeout[0] + tp.connectKey = connectKey + if tp.sock, err = net.Dial("tcp", address); err != nil { + err = fmt.Errorf("ghdc transport: %w", err) + return + } + if err = _handleShake(tp.sock, connectKey); err != nil { + return + } + if alive { + if err = tp.setAlive(); err != nil { + return + } + } + return +} + +func _handleShake(sock net.Conn, connectKey string) (err error) { + data, err := _readN(sock, 48) + if err != nil { + return err + } + if !(data[4] == 79 && data[5] == 72 && data[6] == 79 && data[7] == 83 && data[9] == 72 && data[10] == 68 && data[11] == 67) { + return fmt.Errorf("handle shake error") + } + bannerStr := []byte("OHOS HDC\x00\x00\x00\x00") + connectKeyBytes256 := [256]byte{} + copy(connectKeyBytes256[:], connectKey) + size := 12 + 256 + buffer := new(bytes.Buffer) + if err = binary.Write(buffer, binary.BigEndian, uint32(size)); err != nil { + return fmt.Errorf("transport write: %w", err) + } + if err = binary.Write(buffer, binary.BigEndian, bannerStr); err != nil { + return fmt.Errorf("transport write: %w", err) + } + if err = binary.Write(buffer, binary.BigEndian, connectKeyBytes256); err != nil { + return fmt.Errorf("transport write: %w", err) + } + return _send(sock, buffer.Bytes()) +} + +func (t transport) setAlive() (err error) { + return _sendCommand(t.sock, "alive") +} + +func (t transport) SendCommand(command string) (err error) { + return _sendCommand(t.sock, command) +} + +func _sendCommand(writer io.Writer, command string) (err error) { + command += "\x00" + buf := new(bytes.Buffer) + if err = binary.Write(buf, binary.BigEndian, uint32(len(command))); err != nil { + return err + } + if err = binary.Write(buf, binary.BigEndian, []byte(command)); err != nil { + return err + } + debugLog(fmt.Sprintf("--> %s", command)) + return _send(writer, buf.Bytes()) +} + +func (t transport) SendBytes(data []byte) (err error) { + length := uint32(len(data)) + + newData := make([]byte, 4+len(data)) + + binary.BigEndian.PutUint32(newData[:4], length) + + copy(newData[4:], data) + return _send(t.sock, newData) +} + +func (t transport) ReadStringAll() (s string, err error) { + var raw []byte + raw, err = _readAll(t.sock) + return string(raw), err +} + +func (t transport) ReadAll() (raw []byte, err error) { + return _readAll(t.sock) +} + +func _readAll(reader io.Reader) (raw []byte, err error) { + buffer := new(bytes.Buffer) + for true { + lengthBuf := make([]byte, 4) + _, err := io.ReadFull(reader, lengthBuf) + if err != nil { + if err == io.EOF { + return buffer.Bytes(), nil + } else if errors.Is(err, io.ErrUnexpectedEOF) { + err = fmt.Errorf("reached unexpected EOF, read partial data: %s %v", string(buffer.Bytes()), err) + return nil, err + } else { + return nil, err + } + } + length := binary.BigEndian.Uint32(lengthBuf) + + data, err := _readN(reader, int(length)) + if err != nil { + return nil, err + } + buffer.Write(data) + + } + return buffer.Bytes(), nil +} + +func (t transport) UnpackString() (s string, err error) { + var raw []byte + raw, err = t.UnpackBytes() + return string(raw), err +} + +func (t transport) UnpackBytes() (raw []byte, err error) { + var length string + if length, err = t.ReadStringN(4); err != nil { + return nil, err + } + var size int64 + if size, err = strconv.ParseInt(length, 16, 64); err != nil { + return nil, err + } + + raw, err = t.RehdytesN(int(size)) + debugLog(fmt.Sprintf("\r%s", raw)) + return +} + +func (t transport) ReadStringN(size int) (s string, err error) { + var raw []byte + if raw, err = t.RehdytesN(size); err != nil { + return "", err + } + return string(raw), nil +} + +func (t transport) RehdytesN(size int) (raw []byte, err error) { + _ = t.sock.SetReadDeadline(time.Now().Add(t.readTimeout)) + return _readN(t.sock, size) +} + +func _readResponse(reader io.Reader) error { + raw, err := _readN(reader, DATA_UNIT_LENGTH) + if err != nil { + return fmt.Errorf("failed to read response length: %w", err) + } + if !bytes.Equal(raw[0:4], []byte("OKAY")) { + fmt.Printf("failed to push file: %s\n", string(raw)) + } else { + return nil + } + raw, err = _readAll(reader) + if err != nil { + return fmt.Errorf("failed to read response data: %w", err) + } + return fmt.Errorf("read response error %s", string(raw)) +} + +func (t transport) Close() (err error) { + if t.sock == nil { + return nil + } + return t.sock.Close() +} + +func _send(writer io.Writer, msg []byte) (err error) { + for totalSent := 0; totalSent < len(msg); { + var sent int + if sent, err = writer.Write(msg[totalSent:]); err != nil { + return err + } + if sent == 0 { + return ErrConnBroken + } + totalSent += sent + } + return +} + +func _read(reader io.Reader) (data []byte, err error) { + buf := make([]byte, 4*1024*1024) + n, err := reader.Read(buf) + if err != nil { + return nil, err + } + return buf[:n], nil +} + +func _readN(reader io.Reader, size int) (raw []byte, err error) { + raw = make([]byte, 0, size) + for len(raw) < size { + buf := make([]byte, size-len(raw)) + var n int + if n, err = io.ReadFull(reader, buf); err != nil { + return nil, err + } + if n == 0 { + return nil, ErrConnBroken + } + raw = append(raw, buf...) + } + return +} diff --git a/pkg/ghdc/ui_driver.go b/pkg/ghdc/ui_driver.go new file mode 100644 index 00000000..25990121 --- /dev/null +++ b/pkg/ghdc/ui_driver.go @@ -0,0 +1,927 @@ +package ghdc + +import ( + "embed" + "encoding/json" + "fmt" + "hash/fnv" + "math" + "os" + "path/filepath" + "strings" + "time" +) + +//go:embed agent.so +var agentSO embed.FS + +type UitestRequest struct { + Module string `json:"module,omitempty"` + Method string `json:"method,omitempty"` + Params interface{} `json:"params,omitempty"` + RequestId string `json:"request_id,omitempty"` +} + +type UitestResponse struct { + Result interface{} `json:"result,omitempty"` + Exception *UitestException `json:"exception,omitempty"` +} + +type UitestKitResponse struct { + Result interface{} `json:"result,omitempty"` + Exception string `json:"exception,omitempty"` +} + +type UitestException struct { + Message string `json:"message,omitempty"` + Code int `json:"code,omitempty"` +} + +type UIDriver struct { + Device + uTp *uitestTransport + uKTp *uitestKitTransport +} + +type Dimension struct { + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` +} + +func NewUIDriver(device Device) (d *UIDriver, err error) { + d = new(UIDriver) + d.Device = device + err = d.prepareDevice() + if err != nil { + err = fmt.Errorf("[uitest] failed to prepare device \n%v", err) + return + } + uTp, err := device.createUitestTransport() + if err != nil { + err = fmt.Errorf("[uitest] failed to create uitest transport \n%v", err) + return + } + uKTp, err := device.createUitestKitTransport() + if err != nil { + err = fmt.Errorf("[uitest] failed to create uitest kit transport \n%v", err) + return + } + d.uTp = &uTp + d.uKTp = &uKTp + return +} + +func (d *UIDriver) Close() { + if d.uTp != nil { + d.uTp.Close() + } + if d.uKTp != nil { + d.uKTp.Close() + } +} + +func (d *UIDriver) Reconnect() error { + d.Close() + uTp, err := d.createUitestTransport() + if err != nil { + err = fmt.Errorf("[uitest] failed to create uitest transport \n%v", err) + return err + } + uKTp, err := d.createUitestKitTransport() + if err != nil { + err = fmt.Errorf("[uitest] failed to create uitest kit transport \n%v", err) + return err + } + d.uTp = &uTp + d.uKTp = &uKTp + return nil +} + +func (d *UIDriver) prepareDevice() error { + uitestPid, err := d.Device.RunShellCommand("pidof uitest") + if err != nil { + return err + } + uitestPid = strings.TrimSpace(uitestPid) + + isLowerVersion, err := d.needUpdateLib() + if err != nil { + return err + } + + if uitestPid != "" && !isLowerVersion { + return nil + } + + _, err = d.Device.RunShellCommand("param set persist.ace.testmode.enabled 1") + if err != nil { + return err + } + + if isLowerVersion { + if uitestPid != "" { + _, err = d.Device.RunShellCommand("kill -9 " + uitestPid) + if err != nil { + return err + } + uitestPid = "" + } + + err = d.updateLib() + if err != nil { + return err + } + } + if uitestPid == "" { + _, err = d.Device.RunShellCommand("uitest start-daemon singleness") + if err != nil { + return err + } + } + return nil +} + +func (d *UIDriver) isServerRunning() bool { + res, err := d.Device.RunShellCommand("top -H -n 1 -p $(pidof uitest)") + if err != nil { + return false + } + if strings.Contains(res, "rpc-") { + return true + } + return false +} + +func (d *UIDriver) updateLib() error { + tmpDir := os.TempDir() + soFileName := filepath.Join(tmpDir, "agent.so") + soRaw, err := agentSO.ReadFile("agent.so") + if err != nil { + return err + } + err = os.WriteFile(soFileName, soRaw, os.ModePerm) + if err != nil { + fmt.Println("Error writing file:", err) + return err + } + + _, err = d.Device.RunShellCommand("rm /data/tmp/local/agent.so") + if err != nil { + return err + } + err = d.Device.PushFile(soFileName, "/data/local/tmp/agent.so") + if err != nil { + return err + } + return nil +} + +func (d *UIDriver) needUpdateLib() (res bool, err error) { + deviceVersionStr, err := d.Device.RunShellCommand("cat data/local/tmp/agent.so |grep -a UITEST_AGENT_LIBRARY") + if err != nil { + return false, err + } + soRaw, err := agentSO.ReadFile("agent.so") + if err != nil { + return false, err + } + // 定义要搜索的字符串 + searchString := "UITEST_AGENT_LIBRARY" + + // 将二进制内容转换为字符串 + content := string(soRaw) + + // 按行分割内容 + lines := strings.Split(content, "\n") + + // 搜索包含特定字符串的行 + for _, line := range lines { + if strings.Contains(line, searchString) { + update := false + deviceVersion, err := getVersion(deviceVersionStr) + if err != nil { + update = true + } + lowestVersion, err := getVersion(line) + if err != nil { + return false, err + } + if update || lowestVersion[0] > deviceVersion[0] || lowestVersion[1] > deviceVersion[1] || lowestVersion[2] > deviceVersion[2] { + return true, nil + } + return false, err + } + } + return false, err +} + +func (d *UIDriver) supportDevice() error { + rootDir, err := os.Getwd() + if err != nil { + return err + } + raw, err := os.ReadFile(filepath.Join(rootDir, "minUiTestVersion.txt")) + if err != nil { + return err + } + lowestVersion := string(raw) + uitestVersion, err := d.Device.RunShellCommand("uitest --version") + if lowestVersion > uitestVersion { + return fmt.Errorf("not supprt uitest lowest version %s, device version %s", lowestVersion, uitestVersion) + } + return nil +} + +func (d *UIDriver) createDriver() (driver string, err error) { + params := map[string]interface{}{ + "api": "Driver.create", + "this": nil, + "args": []string{}, + "message_type": "hypium", + } + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + err = fmt.Errorf("[uitest] failed to create driver") + return + } + if res.Exception != nil { + err = fmt.Errorf("[uitest] failed to create driver msg: %s", res.Exception.Message) + return + } + driver = res.Result.(string) + return +} + +func (d *UIDriver) createFocused() (onName string, err error) { + params := map[string]interface{}{ + "api": "On.focused", + "this": "On#seed", + "args": []bool{true}, + "message_type": "hypium", + } + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + err = fmt.Errorf("[uitest] failed to create focused") + return + } + if res.Exception != nil { + err = fmt.Errorf("[uitest] failed to create focused msg: %s", res.Exception.Message) + return + } + onName = res.Result.(string) + return +} + +func (d *UIDriver) findComponent(driverName, onName string) (componentName string, err error) { + params := map[string]interface{}{ + "api": "Driver.waitForComponent", + "this": driverName, + "args": []any{onName, 5000}, + "message_type": "hypium", + } + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + err = fmt.Errorf("[uitest] failed to create focused") + return + } + if res.Exception != nil { + err = fmt.Errorf("[uitest] failed to create focused msg: %s", res.Exception.Message) + return + } + componentName = res.Result.(string) + return +} + +func (d *UIDriver) createPointMatrix(pointerMatrix *PointerMatrix) (pointMatrixName string, err error) { + fingers, steps := pointerMatrix.fingerIndexStats() + params := map[string]interface{}{ + "api": "PointerMatrix.create", + "this": nil, + "args": []int{fingers, steps}, + "message_type": "hypium", + } + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + err = fmt.Errorf("[uitest] failed to create PointerMatrix") + return + } + if res.Exception != nil { + err = fmt.Errorf("[uitest] failed to create PointerMatrix msg: %s", res.Exception.Message) + return + } + pointMatrixName = res.Result.(string) + return +} + +func (d *UIDriver) releaseObj(obj []string) error { + params := map[string]interface{}{ + "api": "BackendObjectsCleaner", + "this": nil, + "args": obj, + } + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + err = fmt.Errorf("[uitest] failed to release driver") + return err + } + if res.Exception != nil { + err = fmt.Errorf("[uitest] failed to release driver msg: %s", res.Exception.Message) + return err + } + return nil +} + +func (d *UIDriver) Touch(x, y int) error { + driverName, err := d.createDriver() + if err != nil { + return err + } + + defer func() { + _ = d.releaseObj([]string{driverName}) + }() + + params := map[string]interface{}{ + "api": "Driver.click", + "this": driverName, + "args": []int{x, y}, + "message_type": "hypium", + } + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + return err + } + if res.Exception != nil { + return fmt.Errorf("[uitest] failed to touch (%d, %d): %s", x, y, res.Exception.Message) + } + return nil +} + +func (d *UIDriver) Drag(fromX, fromY, toX, toY int, duration float64) error { + driverName, err := d.createDriver() + if err != nil { + return err + } + + defer func() { + _ = d.releaseObj([]string{driverName}) + }() + + distance := math.Sqrt(math.Pow(float64(fromX-toX), 2) + math.Pow(float64(fromX-toX), 2)) + speed := int(distance / duration) + + params := map[string]interface{}{ + "api": "Driver.drag", + "this": driverName, + "args": []int{fromX, fromY, toX, toY, speed}, + "message_type": "hypium", + } + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + return err + } + if res.Exception != nil { + return fmt.Errorf("[uitest] failed to Drag from (%d, %d) to (%d, %d): %s", fromX, fromY, toX, toY, res.Exception.Message) + } + return nil +} + +func (d *UIDriver) PressKey(key KeyCode) error { + driverName, err := d.createDriver() + if err != nil { + return err + } + + defer func() { + _ = d.releaseObj([]string{driverName}) + }() + + params := map[string]interface{}{ + "api": "Driver.triggerKey", + "this": driverName, + "args": []KeyCode{key}, + "message_type": "hypium", + } + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + return err + } + if res.Exception != nil { + return fmt.Errorf("[uitest] failed to Press Key code:%d: %s", key, res.Exception.Message) + } + return nil +} + +func (d *UIDriver) PressKeys(keys []KeyCode) error { + driverName, err := d.createDriver() + if err != nil { + return err + } + + defer func() { + _ = d.releaseObj([]string{driverName}) + }() + + params := map[string]interface{}{ + "api": "Driver.triggerCombineKeys", + "this": driverName, + "args": keys, + "message_type": "hypium", + } + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + return err + } + if res.Exception != nil { + return fmt.Errorf("[uitest] failed to Press Key code:%v: %s", keys, res.Exception.Message) + } + return nil +} + +func (d *UIDriver) InjectGesture(gesture *Gesture, speedArg ...int) error { + return d.InjectMultiGesture([]*Gesture{gesture}, speedArg...) +} + +func (d *UIDriver) InjectMultiGesture(gestures []*Gesture, speedArg ...int) error { + speed := 2000 + var releaseObj []string + defer func() { + if len(releaseObj) > 0 { + _ = d.releaseObj(releaseObj) + } + }() + if len(speedArg) > 0 && speedArg[0] > 0 { + speed = speedArg[0] + } + driverName, err := d.createDriver() + if err != nil { + return err + } + releaseObj = append(releaseObj, driverName) + + pointerMatrix := d.gestureToPointMatrix(gestures) + + pointerMatrixName, err := d.createPointMatrix(pointerMatrix) + if err != nil { + return err + } + releaseObj = append(releaseObj, pointerMatrixName) + + for step, point := range pointerMatrix.points { + err = d.setPoint(pointerMatrixName, point.index, step, point.point) + if err != nil { + return err + } + } + + err = d.injectMultiPointerAction(driverName, pointerMatrixName, speed) + if err != nil { + return err + } + + return nil +} + +func (d *UIDriver) InputText(text string) error { + params := map[string]interface{}{ + "api": "Driver.inputText", + "this": nil, + "args": []any{map[string]interface{}{ + "x": 0, + "y": 0, + }, text}, + "message_type": "hypium", + } + + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + return err + } + if res.Exception != nil { + return fmt.Errorf("[uitest] failed to input text %s: %s", text, res.Exception.Message) + } + return nil +} + +func (d *UIDriver) InputTextOnFocused(text string) error { + driverName, err := d.createDriver() + if err != nil { + return err + } + defer func() { + _ = d.releaseObj([]string{driverName}) + }() + + onName, err := d.createFocused() + if err != nil { + return err + } + defer func() { + _ = d.releaseObj([]string{onName}) + }() + componentName, err := d.findComponent(driverName, onName) + if err != nil { + return err + } + defer func() { + _ = d.releaseObj([]string{componentName}) + }() + params := map[string]interface{}{ + "api": "Component.inputText", + "this": componentName, + "args": []string{text}, + "message_type": "hypium", + } + + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + return err + } + if res.Exception != nil { + return fmt.Errorf("[uitest] failed to input text %s: %s", text, res.Exception.Message) + } + return nil +} + +func (d *UIDriver) TouchDown(x, y int) error { + params := map[string]interface{}{ + "api": "touchDown", + "this": nil, + "args": map[string]interface{}{ + "x": x, + "y": y, + }, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "Gestures", DEFAULT, nil) +} + +func (d *UIDriver) TouchDownAsync(x, y int) { + go func() { + err := d.TouchDown(x, y) + if err != nil { + debugLog(fmt.Sprintf("%v", err)) + } + }() +} + +func (d *UIDriver) TouchMove(x, y int) error { + params := map[string]interface{}{ + "api": "touchMove", + "this": nil, + "args": map[string]interface{}{ + "x": x, + "y": y, + }, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "Gestures", DEFAULT, nil) +} + +func (d *UIDriver) TouchMoveAsync(x, y int) { + go func() { + err := d.TouchMove(x, y) + if err != nil { + debugLog(fmt.Sprintf("%v", err)) + } + }() +} + +func (d *UIDriver) TouchUp(x, y int) error { + params := map[string]interface{}{ + "api": "touchUp", + "this": nil, + "args": map[string]interface{}{ + "x": x, + "y": y, + }, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "Gestures", DEFAULT, nil) +} + +func (d *UIDriver) TouchUpAsync(x, y int) { + go func() { + err := d.TouchUp(x, y) + if err != nil { + debugLog(fmt.Sprintf("%v", err)) + } + }() +} + +func (d *UIDriver) PressRecentApp() error { + params := map[string]interface{}{ + "api": "pressRecentApp", + "this": nil, + "args": map[string]interface{}{}, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "Gestures", DEFAULT, nil) +} + +func (d *UIDriver) PressBack() error { + params := map[string]interface{}{ + "api": "pressBack", + "this": nil, + "args": map[string]interface{}{}, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "Gestures", DEFAULT, nil) +} + +func (d *UIDriver) PressPowerKey() error { + params := map[string]interface{}{ + "api": "pressPowerKey", + "this": nil, + "args": map[string]interface{}{}, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "CtrlCmd", DEFAULT, nil) +} + +func (d *UIDriver) GetDisplayRotation() (direction int, err error) { + params := map[string]interface{}{ + "api": "getDisplayRotation", + "this": nil, + "args": map[string]interface{}{}, + "message_type": "hypium", + } + res, err := d.sendUitestKit(params, "CtrlCmd", DEFAULT, nil) + if err != nil { + return + } + if res.Result == false { + err = fmt.Errorf("[uitest] failed to exec method getDisplayRotation msg: %s", res.Exception) + return + } + direction = (int)(res.Result.(float64)) + return direction, err +} + +func (d *UIDriver) UpVolume() error { + params := map[string]interface{}{ + "api": "upVolume", + "this": nil, + "args": map[string]interface{}{}, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "CtrlCmd", DEFAULT, nil) +} + +func (d *UIDriver) DownVolume() error { + params := map[string]interface{}{ + "api": "downVolume", + "this": nil, + "args": map[string]interface{}{}, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "CtrlCmd", DEFAULT, nil) +} + +func (d *UIDriver) RotationDisplay(direction int) error { + params := map[string]interface{}{ + "api": "rotationDisplay", + "this": nil, + "args": map[string]interface{}{ + "direction": direction, + }, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "CtrlCmd", DEFAULT, nil) +} + +func (d *UIDriver) GetDisplaySize() (display Dimension, err error) { + params := map[string]interface{}{ + "api": "getDisplaySize", + "this": nil, + "args": map[string]interface{}{}, + "message_type": "hypium", + } + res, err := d.sendUitestKit(params, "CtrlCmd", DEFAULT, nil) + if err != nil { + return + } + if res.Result == false { + err = fmt.Errorf("[uitest] failed to exec method getDisplaySize msg: %s", res.Exception) + return + } + raw, err := json.Marshal(res.Result) + if err != nil { + return + } + err = json.Unmarshal(raw, &display) + return +} + +func (d *UIDriver) StartCaptureScreen(callback UitestKitCallback) error { + params := map[string]interface{}{ + "api": "startCaptureScreen", + "this": nil, + "args": map[string]interface{}{}, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "Captures", SCREEN_CAPTURE, callback) +} + +func (d *UIDriver) StopCaptureScreen() error { + params := map[string]interface{}{ + "api": "stopCaptureScreen", + "this": nil, + "args": map[string]interface{}{}, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "Captures", DEFAULT, nil) +} + +func (d *UIDriver) StartCaptureUiAction(callback UitestKitCallback) error { + params := map[string]interface{}{ + "api": "startCaptureUiAction", + "this": nil, + "args": map[string]interface{}{}, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "Captures", UI_ACTION_CAPTURE, callback) +} + +func (d *UIDriver) StopCaptureUiAction() error { + params := map[string]interface{}{ + "api": "stopCaptureUiAction", + "this": nil, + "args": map[string]interface{}{}, + "message_type": "hypium", + } + return d.sendUitestKitNoResult(params, "Captures", DEFAULT, nil) +} + +func (d *UIDriver) CaptureLayout() (layout interface{}, err error) { + params := map[string]interface{}{ + "api": "captureLayout", + "this": nil, + "args": map[string]interface{}{}, + "message_type": "hypium", + } + res, err := d.sendUitestKit(params, "Captures", DEFAULT, nil) + if err != nil { + return + } + if res.Result == false { + err = fmt.Errorf("[uitest] failed to exec method captureLayout msg: %s", res.Exception) + return + } + return res.Result, err +} + +func (d *UIDriver) sendUitestKitNoResult(params map[string]interface{}, method string, reqType ReqTypeEnum, callback UitestKitCallback) error { + res, err := d.sendUitestKit(params, method, reqType, callback) + if err != nil { + return err + } + if res.Result == false { + err = fmt.Errorf("[uitest] failed to exec method %s params %v msg: %s", method, params, res.Exception) + return err + } + + return nil +} + +func (d *UIDriver) sendUitestReq(req UitestRequest) (res UitestResponse, err error) { + res, err = d.uTp.SendReq(req) + if err != nil { + fmt.Printf("[uitest] failed to send req first. try reconnect \n%v \n", err) + if err = d.Reconnect(); err != nil { + return + } + res, err = d.uTp.SendReq(req) + } + return +} + +func (d *UIDriver) sendUitestKit(params map[string]interface{}, method string, reqType ReqTypeEnum, callback UitestKitCallback) (response UitestKitResponse, err error) { + request := newHypiumRequest(params, method) + requestStr, err := request.ToString() + if err != nil { + err = fmt.Errorf("[uitest] failed to create req while exec method %s %v", method, err) + return + } + sessionId := hashCode(fmt.Sprintf("%s%d", requestStr, time.Now().Unix())) + if sessionId <= (1 << 24) { + sessionId += 1 << 24 + } + err = d.uKTp.registerCallback(reqType, sessionId, nil) + if err != nil { + fmt.Printf("[uitest] failed to register callback try reconnect %s %v", method, err) + if err = d.Reconnect(); err != nil { + return + } + if err = d.uKTp.registerCallback(reqType, sessionId, nil); err != nil { + return + } + } + res, err := d.uKTp.sendMessage(reqType, sessionId, requestStr) + if err != nil { + err = fmt.Errorf("[uitest] failed to send message while exec method %s sessionId: %d %v", method, sessionId, err) + return + } + err = d.uKTp.registerCallback(reqType, sessionId, callback) + if err != nil { + err = fmt.Errorf("[uitest] failed to register callback while exec method %s %v", method, err) + return + } + return res, nil +} + +func (d *UIDriver) gestureToPointMatrix(gestures []*Gesture) *PointerMatrix { + pointerMatrix := &PointerMatrix{} + for fingerIndex, gestures := range gestures { + var curPoint Point + for _, gestureStep := range gestures.steps { + if gestureStep.GestureType == "start" { + pointerMatrix.setPoint(gestureStep.Point, fingerIndex, gestureStep.Duration) + curPoint = gestureStep.Point + } + if gestureStep.GestureType == "move" { + toPoint := gestureStep.Point + offsetX := toPoint.X - curPoint.X + offsetY := toPoint.Y - curPoint.Y + steps := gestureStep.calculateSteps() + for i := 0; i < steps-1; i++ { + curPoint = Point{X: curPoint.X + (offsetX / steps), Y: curPoint.Y + (offsetY / steps)} + pointerMatrix.setPoint(curPoint, fingerIndex, EVENT_INJECTION_DELAY_MS) + } + curPoint = toPoint + if steps == 1 { + pointerMatrix.setPoint(curPoint, fingerIndex, gestureStep.Duration%EVENT_INJECTION_DELAY_MS) + } else { + pointerMatrix.setPoint(curPoint, fingerIndex, EVENT_INJECTION_DELAY_MS+(gestureStep.Duration%EVENT_INJECTION_DELAY_MS)) + } + } + if gestureStep.GestureType == "pause" { + steps := gestureStep.calculateSteps() + for i := 0; i < steps-1; i++ { + pointerMatrix.setPoint(curPoint, fingerIndex, EVENT_INJECTION_DELAY_MS) + } + pointerMatrix.setPoint(curPoint, fingerIndex, EVENT_INJECTION_DELAY_MS+(gestureStep.Duration%EVENT_INJECTION_DELAY_MS)) + } + } + } + return pointerMatrix +} + +func (d *UIDriver) setPoint(pointerMatrixName string, fingerIndex int, step int, point Point) error { + params := map[string]interface{}{ + "api": "PointerMatrix.setPoint", + "this": pointerMatrixName, + "args": []any{fingerIndex, step, point}, + "message_type": "hypium", + } + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + return err + } + if res.Exception != nil { + return fmt.Errorf("[uitest] failed to setPoint from: %s", res.Exception.Message) + } + return nil +} + +func (d *UIDriver) injectMultiPointerAction(driverName, pointerMatrixName string, speed int) error { + params := map[string]interface{}{ + "api": "Driver.injectMultiPointerAction", + "this": driverName, + "args": []any{pointerMatrixName, speed}, + "message_type": "hypium", + } + res, err := d.sendUitestReq(newHypiumRequest(params, "callHypiumApi")) + if err != nil { + return err + } + if res.Exception != nil { + return fmt.Errorf("[uitest] failed to injectMultiPointerAction from: %s", res.Exception.Message) + } + return nil +} + +func hashCode(s string) uint32 { + h := fnv.New32a() + _, _ = h.Write([]byte(s)) + return h.Sum32() +} + +func (r UitestRequest) ToString() (result string, err error) { + data, err := json.Marshal(r) + if err != nil { + err = fmt.Errorf("error: \n%v", err) + return + } + return string(data), nil +} + +func getVersion(str string) (version []string, err error) { + index := strings.Index(str, "@") + if index == -1 { + err = fmt.Errorf("invalid version str") + return + } + version = strings.Split(str[index+1:], ".") + return +} diff --git a/pkg/ghdc/ui_driver_test.go b/pkg/ghdc/ui_driver_test.go new file mode 100644 index 00000000..ba6e2f9b --- /dev/null +++ b/pkg/ghdc/ui_driver_test.go @@ -0,0 +1,274 @@ +package ghdc + +import ( + "fmt" + "sync" + "testing" + "time" +) + +var ( + client Client + device Device + driver *UIDriver +) + +func setUp(t *testing.T) { + var err error + client, err = NewClient() + if err != nil { + t.Fatal(err) + } + + devices, err := client.DeviceList() + if err != nil { + t.Fatal(err) + } + if len(devices) == 0 { + t.Fatal("not found available device") + } + device = devices[0] + driver, err = NewUIDriver(device) + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_Touch(t *testing.T) { + setUp(t) + err := driver.Touch(1038, 798) + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_Drag(t *testing.T) { + setUp(t) + err := driver.Drag(800, 1000, 200, 1000, 0.2) + if err != nil { + t.Fatal(err) + } +} + +type CaptureScreenCallback struct { + count int + mux sync.Mutex +} + +// OnData handles the data received +func (cb *CaptureScreenCallback) OnData(data []byte) { + cb.mux.Lock() + cb.count++ + cb.mux.Unlock() + fmt.Printf("Data received: %s\n", string(data)) +} + +// onError handles the error received +func (cb *CaptureScreenCallback) OnError(err error) { + fmt.Printf("Error received: %v\n", err) +} + +func (cb *CaptureScreenCallback) startCounter() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for range ticker.C { + cb.mux.Lock() + fmt.Printf("Screen Data received count in the last second: %d\n", cb.count) + cb.count = 0 + cb.mux.Unlock() + } +} + +func TestDevice_StartCaptureScreen(t *testing.T) { + setUp(t) + err := driver.StopCaptureScreen() + if err != nil { + t.Fatal(err) + } + callback := &CaptureScreenCallback{} + err = driver.StartCaptureScreen(callback) + if err != nil { + t.Fatal(err) + } + callback.startCounter() + time.Sleep(1 * time.Minute) +} + +func TestDevice_StartCaptureUIAction(t *testing.T) { + setUp(t) + err := driver.StopCaptureUiAction() + if err != nil { + t.Fatal(err) + } + callback := &CaptureScreenCallback{} + err = driver.StartCaptureUiAction(callback) + if err != nil { + t.Fatal(err) + } + time.Sleep(1 * time.Minute) +} + +func TestDevice_TouchDownMoveUp(t *testing.T) { + setUp(t) + + for i := 0; i < 10000; i++ { + // time.Sleep(1 * time.Second) + debugLog(fmt.Sprintf("running... time %d", i)) + err := driver.TouchDown(225, 1700) + if err != nil { + debugLog(err.Error()) + // t.Fatal(err) + } + time.Sleep(20 * time.Millisecond) + err = driver.TouchMove(325, 1700) + if err != nil { + debugLog(err.Error()) + // t.Fatal(err) + } + time.Sleep(20 * time.Millisecond) + err = driver.TouchMove(425, 1700) + if err != nil { + debugLog(err.Error()) + // t.Fatal(err) + } + time.Sleep(20 * time.Millisecond) + err = driver.TouchMove(525, 1700) + if err != nil { + debugLog(err.Error()) + // t.Fatal(err) + } + err = driver.TouchUp(625, 1700) + if err != nil { + debugLog(err.Error()) + // t.Fatal(err) + } + } +} + +func TestDevice_TouchDownUp(t *testing.T) { + setUp(t) + err := driver.TouchDown(200, 2000) + if err != nil { + debugLog(err.Error()) + } + err = driver.TouchUp(200, 2000) + if err != nil { + debugLog(err.Error()) + t.Fatal(err) + } +} + +func TestDevice_TouchDownMoveUpAsync(t *testing.T) { + setUp(t) + driver.TouchDown(225, 1700) + time.Sleep(60 * time.Millisecond) + driver.TouchMoveAsync(325, 1700) + time.Sleep(60 * time.Millisecond) + driver.TouchMoveAsync(425, 1700) + time.Sleep(60 * time.Millisecond) + driver.TouchMoveAsync(525, 1700) + time.Sleep(60 * time.Millisecond) + driver.TouchUpAsync(625, 1700) + time.Sleep(4 * time.Second) +} + +func TestDevice_GetDisplay(t *testing.T) { + setUp(t) + display, err := driver.GetDisplaySize() + if err != nil { + t.Fatal(err) + } + t.Log(display) +} + +func TestDevice_GetRotation(t *testing.T) { + setUp(t) + rotation, err := driver.GetDisplayRotation() + if err != nil { + t.Fatal(err) + } + t.Log(rotation) +} + +func TestDevice_PressRecentApp(t *testing.T) { + setUp(t) + err := driver.PressRecentApp() + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_PressBack(t *testing.T) { + setUp(t) + err := driver.PressBack() + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_PressPowerKey(t *testing.T) { + setUp(t) + err := driver.PressPowerKey() + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_UpVolume(t *testing.T) { + setUp(t) + err := driver.UpVolume() + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_DownVolume(t *testing.T) { + setUp(t) + err := driver.DownVolume() + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_CaptureLayout(t *testing.T) { + setUp(t) + layout, err := driver.CaptureLayout() + if err != nil { + t.Fatal(err) + } + t.Log(layout) +} + +func TestDevice_InputText(t *testing.T) { + setUp(t) + err := driver.InputText("abcdef") + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_InjectPoint(t *testing.T) { + setUp(t) + err := driver.InjectGesture(NewGesture().Start(Point{800, 2000}).MoveTo(Point{200, 2000}, 2000)) + if err != nil { + t.Fatal(err) + } + time.Sleep(2 * time.Second) +} + +func TestDevice_PressKey(t *testing.T) { + setUp(t) + err := driver.PressKey(KEYCODE_NUM_1) + if err != nil { + t.Fatal(err) + } +} + +func TestDevice_PressKeys(t *testing.T) { + setUp(t) + err := driver.PressKeys([]KeyCode{KEYCODE_SHIFT_LEFT, KEYCODE_NUM_1}) + if err != nil { + t.Fatal(err) + } +} diff --git a/pkg/ghdc/ui_gesture.go b/pkg/ghdc/ui_gesture.go new file mode 100644 index 00000000..eaa969bd --- /dev/null +++ b/pkg/ghdc/ui_gesture.go @@ -0,0 +1,83 @@ +package ghdc + +import ( + "math" +) + +var EVENT_INJECTION_DELAY_MS = 50 + +type Point struct { + X int `json:"x,omitempty"` + Y int `json:"y,omitempty"` +} + +func (p Point) distance(p0 Point) float64 { + return math.Sqrt(math.Pow(float64(p.X-p0.X), 2) + math.Pow(float64(p.Y-p0.Y), 2)) +} + +type GestureStep struct { + Point + GestureType string + Duration int +} + +type EmptyGesture struct{} + +type Gesture struct { + steps []*GestureStep +} + +type PointerMatrix struct { + points []*FingerPoint +} + +type FingerPoint struct { + index int + point Point +} + +func NewGesture() *EmptyGesture { + return new(EmptyGesture) +} + +func (g *EmptyGesture) Start(point Point) *Gesture { + gesture := &Gesture{} + gesture.steps = append(gesture.steps, &GestureStep{Point: point, GestureType: "start"}) + return gesture +} + +func (g *Gesture) MoveTo(point Point, duration int) *Gesture { + g.steps = append(g.steps, &GestureStep{Point: point, GestureType: "move", Duration: duration}) + return g +} + +func (g *Gesture) Pause(duration int) *Gesture { + g.steps = append(g.steps, &GestureStep{GestureType: "pause", Duration: duration}) + return g +} + +func (gs *GestureStep) calculateSteps() int { + return (gs.Duration / EVENT_INJECTION_DELAY_MS) + 1 +} + +func (pm *PointerMatrix) setPoint(point Point, fingerIndex, duration int) { + point.X += 65536 * duration + pm.points = append(pm.points, &FingerPoint{index: fingerIndex, point: point}) +} + +func (pm *PointerMatrix) fingerIndexStats() (fingers int, maxSteps int) { + indexMap := make(map[int]int) + + for _, fp := range pm.points { + indexMap[fp.index]++ + } + + fingers = len(indexMap) + for _, count := range indexMap { + if count > maxSteps { + maxSteps = count + } + } + + return fingers, maxSteps +} diff --git a/pkg/ghdc/uitest_kit_transport.go b/pkg/ghdc/uitest_kit_transport.go new file mode 100644 index 00000000..9192412f --- /dev/null +++ b/pkg/ghdc/uitest_kit_transport.go @@ -0,0 +1,345 @@ +package ghdc + +import ( + "bytes" + "container/list" + "encoding/binary" + "encoding/json" + "fmt" + "net" + "strconv" + "sync" + "time" +) + +type uitestKitTransport struct { + connectionPool *ConnectionPool + socketMap map[string]*SocketContext + serial string + mu sync.Mutex +} + +type SocketContext struct { + conn net.Conn + socketId string + writeLock sync.Mutex + + callbackMap map[string]UitestKitCallback + queue *responseList +} + +type response struct { + sessionId uint32 + payload []byte +} + +type responseList struct { + list *list.List + lock sync.Mutex +} + +func (r *responseList) Clear() { + r.lock.Lock() + defer r.lock.Unlock() + r.list.Init() +} + +func (r *responseList) push(v interface{}) { + defer r.lock.Unlock() + r.lock.Lock() + r.list.PushBack(v) +} + +func (r *responseList) traverse(action func(response response)) { + r.lock.Lock() + defer r.lock.Unlock() + for e := r.list.Front(); e != nil; e = e.Next() { + action(e.Value.(response)) + } +} + +func (r *responseList) remove(sessionId uint32) *response { + r.lock.Lock() + defer r.lock.Unlock() + for e := r.list.Front(); e != nil; e = e.Next() { + response := e.Value.(response) + if response.sessionId == sessionId { + r.list.Remove(e) + return &response + } + } + return nil +} + +type UitestKitCallback interface { + OnData([]byte) + OnError(error) +} + +type ReqTypeEnum int + +const ( + DEFAULT ReqTypeEnum = iota + SCREEN_CAPTURE + UI_ACTION_CAPTURE +) + +const ( + HEADER_BYTES = "_uitestkit_rpc_message_head_" + TAILER_BYTES = "_uitestkit_rpc_message_tail_" +) + +func newUitestKitTransport(serial string, host string, port string) (uKtp uitestKitTransport, err error) { + pool, err := newConnectionPool(host, port, 3) + if err != nil { + err = fmt.Errorf("[uitest] failed to init connection pool \n%v", err) + return + } + uKtp.connectionPool = pool + uKtp.socketMap = make(map[string]*SocketContext) + uKtp.serial = serial + return +} + +func (uKtp *uitestKitTransport) initializeSocket(reqType ReqTypeEnum) error { + socketId := fmt.Sprintf("socket_%d_%s", reqType, uKtp.serial) + uKtp.mu.Lock() + defer uKtp.mu.Unlock() + if uKtp.socketMap[socketId] != nil { + return nil + } + connection, err := uKtp.connectionPool.getConnection() + if err != nil { + return err + } + + uKtp.socketMap[socketId] = &SocketContext{socketId: socketId, conn: connection, callbackMap: make(map[string]UitestKitCallback), queue: newResponseList()} + go uKtp.receive(reqType) + return nil +} + +func (uKtp *uitestKitTransport) disconnect(reqType ReqTypeEnum) { + socketId := fmt.Sprintf("socket_%d_%s", reqType, uKtp.serial) + uKtp.mu.Lock() + defer uKtp.mu.Unlock() + socketContext := uKtp.socketMap[socketId] + if socketContext == nil { + return + } + socketContext.Close() + delete(uKtp.socketMap, socketId) +} + +func (uKtp *uitestKitTransport) registerCallback(reqType ReqTypeEnum, sessionId uint32, callback UitestKitCallback) error { + socketId := fmt.Sprintf("socket_%d_%s", reqType, uKtp.serial) + socketContext := uKtp.socketMap[socketId] + if socketContext == nil { + if err := uKtp.initializeSocket(reqType); err != nil { + return err + } + socketContext = uKtp.socketMap[socketId] + } + for { + socketContext.writeLock.Lock() + res := socketContext.queue.remove(sessionId) + socketContext.writeLock.Unlock() + if res != nil && callback != nil { + callback.OnData(res.payload) + } + if res == nil { + break + } + } + socketContext.writeLock.Lock() + socketContext.callbackMap[strconv.Itoa(int(sessionId))] = callback + socketContext.writeLock.Unlock() + return nil +} + +func (uKtp *uitestKitTransport) receive(reqType ReqTypeEnum) { + socketId := fmt.Sprintf("socket_%d_%s", reqType, uKtp.serial) + defer uKtp.disconnect(reqType) + socketContext := uKtp.socketMap[socketId] + var receiveError error + defer func() { + if receiveError != nil { + socketContext.onException(receiveError) + } + }() + if socketContext == nil { + if receiveError = uKtp.initializeSocket(reqType); receiveError != nil { + return + } + socketContext = uKtp.socketMap[socketId] + } + + for { + headerSize := len(HEADER_BYTES) + raw, err := _readN(socketContext.conn, headerSize+8) + if err != nil { + receiveError = err + break + } + if len(raw) == 0 { + continue + } + + header := raw[:len(HEADER_BYTES)] + if !bytes.Equal(header, []byte(HEADER_BYTES)) { + receiveError = fmt.Errorf("verify message head failed on channel: %s", socketId) + break + } + var sessionId, length uint32 + if receiveError = binary.Read(bytes.NewReader(raw[headerSize:headerSize+4]), binary.BigEndian, &sessionId); receiveError != nil { + break + } + if receiveError = binary.Read(bytes.NewReader(raw[headerSize+4:headerSize+8]), binary.BigEndian, &length); receiveError != nil { + break + } + payload, err := _readN(socketContext.conn, int(length)) + if err != nil { + receiveError = err + break + } + tail, err := _readN(socketContext.conn, len(TAILER_BYTES)) + if err != nil { + receiveError = err + break + } + if !bytes.Equal(tail, []byte(TAILER_BYTES)) { + receiveError = fmt.Errorf("verify message tail failed on channel: %s", socketId) + break + } + socketContext.writeLock.Lock() + callback := socketContext.callbackMap[strconv.Itoa(int(sessionId))] + socketContext.writeLock.Unlock() + if callback != nil { + callback.OnData(payload) + } else { + socketContext.queue.push(response{sessionId: sessionId, payload: payload}) + } + } +} + +func (uKtp *uitestKitTransport) sendMessage(reqType ReqTypeEnum, sessionId uint32, message string) (response UitestKitResponse, err error) { + defer func() { + if err != nil { + uKtp.disconnect(reqType) + } + }() + if err = uKtp._sendMessage(reqType, sessionId, message); err != nil { + return + } + return uKtp.receiveMessage(reqType, sessionId) +} + +func (uKtp *uitestKitTransport) _sendMessage(reqType ReqTypeEnum, sessionId uint32, message string) (err error) { + socketId := fmt.Sprintf("socket_%d_%s", reqType, uKtp.serial) + socketContext := uKtp.socketMap[socketId] + if socketContext == nil { + if err = uKtp.initializeSocket(reqType); err != nil { + return + } + socketContext = uKtp.socketMap[socketId] + } + buffer := new(bytes.Buffer) + if err = binary.Write(buffer, binary.BigEndian, []byte(HEADER_BYTES)); err != nil { + return + } + if err = binary.Write(buffer, binary.BigEndian, sessionId); err != nil { + return + } + if err = binary.Write(buffer, binary.BigEndian, uint32(len(message))); err != nil { + return + } + if err = binary.Write(buffer, binary.BigEndian, []byte(message)); err != nil { + return + } + if err = binary.Write(buffer, binary.BigEndian, []byte(TAILER_BYTES)); err != nil { + return + } + socketContext.writeLock.Lock() + defer socketContext.writeLock.Unlock() + return _send(socketContext.conn, buffer.Bytes()) +} + +func (sc *SocketContext) Close() { + sc.writeLock.Lock() + defer sc.writeLock.Unlock() + + if sc.conn != nil { + _ = sc.conn.Close() + } + + // 清空callbackMap + for key := range sc.callbackMap { + delete(sc.callbackMap, key) + } + sc.callbackMap = nil + + // 清理队列 + if sc.queue != nil { + sc.queue.Clear() + } +} + +func (uKtp *uitestKitTransport) Close() { + uKtp.mu.Lock() + defer uKtp.mu.Unlock() + + // 关闭所有的SocketContext + if uKtp.socketMap != nil { + for _, socketContext := range uKtp.socketMap { + socketContext.Close() + } + } + + // 关闭连接池 + if uKtp.connectionPool != nil { + uKtp.connectionPool.close() + } + + uKtp.socketMap = nil + uKtp.connectionPool = nil +} + +func (uKtp *uitestKitTransport) receiveMessage(reqType ReqTypeEnum, sessionId uint32) (response UitestKitResponse, err error) { + socketId := fmt.Sprintf("socket_%d_%s", reqType, uKtp.serial) + socketContext := uKtp.socketMap[socketId] + if socketContext == nil { + err = fmt.Errorf("failed to read message. not found target connection") + return + } + // 创建一个计时器,设置超时时间为 10 秒 + timeout := time.After(2 * time.Second) + + // 创建一个 ticker,每秒触发一次 + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-timeout: + err = fmt.Errorf("failed to read message in 2 second") + return + case <-ticker.C: + res := socketContext.queue.remove(sessionId) + if res != nil { + err = json.Unmarshal(res.payload, &response) + return + } + } + } +} + +func (sc *SocketContext) onException(err error) { + for _, callback := range sc.callbackMap { + if callback != nil { + callback.OnError(err) + } + } +} + +func newResponseList() *responseList { + return &responseList{list: list.New()} +} diff --git a/pkg/ghdc/uitest_transport.go b/pkg/ghdc/uitest_transport.go new file mode 100644 index 00000000..b9abc943 --- /dev/null +++ b/pkg/ghdc/uitest_transport.go @@ -0,0 +1,82 @@ +package ghdc + +import ( + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "time" +) + +type uitestTransport struct { + connectionPool *ConnectionPool + readTimeout time.Duration +} + +func newUitestTransport(host string, port string, readTimeout ...time.Duration) (uTp uitestTransport, err error) { + if len(readTimeout) == 0 { + readTimeout = []time.Duration{DefaultReadTimeout} + } + uTp.readTimeout = readTimeout[0] + pool, err := newConnectionPool(host, port, 2) + if err != nil { + err = fmt.Errorf("[uitest] failed to init connection pool \n%v", err) + return + } + uTp.connectionPool = pool + return +} + +func newHypiumRequest(params interface{}, method string) UitestRequest { + return UitestRequest{ + Module: "com.ohos.devicetest.hypiumApiHelper", + Method: method, + Params: params, + RequestId: MD5(fmt.Sprintf("%d", time.Now().UnixNano())), + } +} + +func (uTp *uitestTransport) SendReq(req UitestRequest) (res UitestResponse, err error) { + requestBytes, err := json.Marshal(req) + if err != nil { + err = fmt.Errorf("[uitest] failed to marshal %v request %v", req, err) + return + } + requestBytes = append(requestBytes, '\n') + conn, err := uTp.connectionPool.getConnection() + if err != nil { + err = fmt.Errorf("[uitest] failed to get connection \n%v", err) + return + } + defer func() { + uTp.connectionPool.releaseConnection(conn) + }() + _ = conn.SetReadDeadline(time.Now().Add(uTp.readTimeout)) + debugLog(fmt.Sprintf("[uitest] send Request %v", req)) + err = _send(conn, requestBytes) + if err != nil { + err = fmt.Errorf("[uitest] failed to get send %v request %v", req, err) + return + } + raw, err := _read(conn) + if err != nil { + err = fmt.Errorf("[uitest] failed to read %v response %v", req, err) + return + } + res = UitestResponse{} + err = json.Unmarshal(raw, &res) + return +} + +func (uTp *uitestTransport) Close() { + if uTp.connectionPool != nil { + uTp.connectionPool.close() + } + uTp.connectionPool = nil +} + +func MD5(str string) string { + hash := md5.New() + hash.Write([]byte(str)) + return hex.EncodeToString(hash.Sum(nil)) +}