Files
httprunner/pkg/ghdc/ui_driver.go
2025-03-05 21:40:47 +08:00

928 lines
23 KiB
Go

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
}