diff --git a/internal/version/VERSION b/internal/version/VERSION index 48669bd5..61d0e9f0 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0+2502182022 +v5.0.0+2502182046 diff --git a/server/ext/app.go b/server/ext/app.go new file mode 100644 index 00000000..32880b8c --- /dev/null +++ b/server/ext/app.go @@ -0,0 +1,63 @@ +package server_ext + +import ( + "fmt" + "os" + + "github.com/gin-gonic/gin" + + "github.com/httprunner/httprunner/v5/internal/builtin" + "github.com/httprunner/httprunner/v5/pkg/uixt" + "github.com/httprunner/httprunner/v5/server" +) + +func installAppHandler(c *gin.Context) { + var appInstallReq AppInstallRequest + if err := c.ShouldBindJSON(&appInstallReq); err != nil { + server.RenderErrorValidateRequest(c, err) + return + } + driver, err := GetDriver(c) + if err != nil { + return + } + err = driver.InstallByUrl(appInstallReq.AppUrl) + if err != nil { + server.RenderError(c, err) + return + } + if androidDevice, ok := driver.GetDevice().(*uixt.AndroidDevice); ok { + _ = driver.Home() + if appInstallReq.MappingUrl == "" || appInstallReq.ResourceMappingUrl == "" { + server.RenderSuccess(c, true) + return + } + localMappingPath, err := builtin.DownloadFileByUrl(appInstallReq.MappingUrl) + if err != nil { + server.RenderError(c, err) + } + defer func() { + _ = os.Remove(localMappingPath) + }() + if err = androidDevice.PushFile( + localMappingPath, + fmt.Sprintf("/data/local/tmp/%s_map.txt", appInstallReq.PackageName)); err != nil { + server.RenderError(c, err) + return + } + localResourceMappingPath, err := builtin.DownloadFileByUrl( + appInstallReq.ResourceMappingUrl) + if err != nil { + server.RenderError(c, err) + } + defer func() { + _ = os.Remove(localResourceMappingPath) + }() + if err = androidDevice.PushFile(localResourceMappingPath, + fmt.Sprintf("/data/local/tmp/%s_resmap.txt", appInstallReq.PackageName)); err != nil { + server.RenderError(c, err) + return + } + } + server.RenderSuccess(c, true) +} diff --git a/server/ext/context.go b/server/ext/context.go new file mode 100644 index 00000000..10e26ea4 --- /dev/null +++ b/server/ext/context.go @@ -0,0 +1,41 @@ +package server_ext + +import ( + "strings" + + "github.com/gin-gonic/gin" + + "github.com/httprunner/httprunner/v5/pkg/uixt" + "github.com/httprunner/httprunner/v5/pkg/uixt/ai" + "github.com/httprunner/httprunner/v5/pkg/uixt/driver_ext" + "github.com/httprunner/httprunner/v5/server" +) + +func GetDriver(c *gin.Context) (driverExt *driver_ext.XTDriver, err error) { + platform := c.Param("platform") + deviceObj, exists := c.Get("device") + var device uixt.IDevice + var driver uixt.IDriver + if !exists { + device, err = server.GetDevice(c) + if err != nil { + return nil, err + } + } else { + device = deviceObj.(uixt.IDevice) + } + switch strings.ToLower(platform) { + case "android": + driver, err = driver_ext.NewStubAndroidDriver(device.(*uixt.AndroidDevice)) + case "ios": + driver, err = driver_ext.NewStubIOSDriver(device.(*uixt.IOSDevice)) + } + if err != nil { + server.RenderErrorInitDriver(c, err) + return + } + c.Set("driver", driver) + driverExt = driver_ext.NewXTDriver(driver, + ai.WithCVService(ai.CVServiceTypeVEDEM)) + return driverExt, nil +} diff --git a/server/ext/handler.go b/server/ext/handler.go new file mode 100644 index 00000000..09b932dd --- /dev/null +++ b/server/ext/handler.go @@ -0,0 +1,62 @@ +package server_ext + +import ( + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v5/pkg/uixt/driver_ext" + "github.com/httprunner/httprunner/v5/server" +) + +func loginHandler(c *gin.Context) { + var loginReq LoginRequest + if err := c.ShouldBindJSON(&loginReq); err != nil { + server.RenderErrorValidateRequest(c, err) + return + } + + driver, err := GetDriver(c) + if err != nil { + return + } + info, err := driver.IDriver.(driver_ext.IStubDriver). + LoginNoneUI(loginReq.PackageName, loginReq.PhoneNumber, + loginReq.Captcha, loginReq.Password) + if err != nil { + server.RenderError(c, err) + return + } + server.RenderSuccess(c, info) +} + +func logoutHandler(c *gin.Context) { + var logoutReq LogoutRequest + if err := c.ShouldBindJSON(&logoutReq); err != nil { + server.RenderErrorValidateRequest(c, err) + return + } + + driver, err := GetDriver(c) + if err != nil { + return + } + err = driver.IDriver.(driver_ext.IStubDriver). + LogoutNoneUI(logoutReq.PackageName) + if err != nil { + server.RenderError(c, err) + return + } + server.RenderSuccess(c, true) +} + +func sourceHandler(c *gin.Context) { + driver, err := GetDriver(c) + if err != nil { + return + } + source, err := driver.Source() + if err != nil { + log.Warn().Err(err).Msg("get source failed") + } + server.RenderSuccess(c, source) +} diff --git a/server/ext/main.go b/server/ext/main.go new file mode 100644 index 00000000..11de3e8c --- /dev/null +++ b/server/ext/main.go @@ -0,0 +1,16 @@ +package server_ext + +import ( + "github.com/httprunner/httprunner/v5/server" +) + +func NewExtRouter() *server.Router { + router := server.NewRouter() + apiV1PlatformSerial := router.Group("/api/v1").Group("/:platform").Group("/:serial") + + apiV1PlatformSerial.GET("/stub/source", sourceHandler) + apiV1PlatformSerial.POST("/stub/login", loginHandler) + apiV1PlatformSerial.POST("/stub/logout", logoutHandler) + apiV1PlatformSerial.POST("/app/install", installAppHandler) + return router +} diff --git a/server/ext/model.go b/server/ext/model.go new file mode 100644 index 00000000..4afa6caf --- /dev/null +++ b/server/ext/model.go @@ -0,0 +1,25 @@ +package server_ext + +type AppInstallRequest struct { + AppUrl string `json:"appUrl" binding:"required"` + MappingUrl string `json:"mappingUrl"` + ResourceMappingUrl string `json:"resourceMappingUrl"` + PackageName string `json:"packageName"` +} + +type LoginRequest struct { + PackageName string `json:"packageName"` + PhoneNumber string `json:"phoneNumber" binding:"required"` + Captcha string `json:"captcha" binding:"required_without=Password"` + Password string `json:"password" binding:"required_without=Captcha"` +} + +type LogoutRequest struct { + PackageName string `json:"packageName"` +} + +type HttpResponse struct { + Code int `json:"code"` + Message string `json:"msg"` + Result interface{} `json:"result,omitempty"` +} diff --git a/server/ext/shoots.go b/server/ext/shoots.go deleted file mode 100644 index 897d9061..00000000 --- a/server/ext/shoots.go +++ /dev/null @@ -1,241 +0,0 @@ -package server_ext - -import ( - "fmt" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v5/code" - "github.com/httprunner/httprunner/v5/pkg/uixt" - "github.com/httprunner/httprunner/v5/pkg/uixt/driver_ext" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" - "github.com/httprunner/httprunner/v5/server" -) - -func NewExtServer(port int) error { - router := server.NewRouter() - apiV1PlatformSerial := router.Group("/api/v1").Group("/:platform").Group("/:serial") - - // shoots operations - apiV1PlatformSerial.GET("/shoots/source", handleDeviceContext(), sourceHandler) - apiV1PlatformSerial.POST("/shoots/login", handleDeviceContext(), loginHandler) - apiV1PlatformSerial.POST("/shoots/logout", handleDeviceContext(), logoutHandler) - - err := router.Engine.Run(fmt.Sprintf("127.0.0.1:%d", port)) - if err != nil { - log.Err(err).Msg("failed to start http server") - return err - } - return nil -} - -func sourceHandler(c *gin.Context) { - dExt, err := getContextDriver(c) - if err != nil { - return - } - - app, err := dExt.ForegroundInfo() - if err != nil { - log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get foreground app", c.HandlerName())) - c.JSON(http.StatusInternalServerError, - server.HttpResponse{ - Code: code.GetErrorCode(err), - Message: err.Error(), - }, - ) - c.Abort() - return - } - source, err := dExt.Source(option.WithProcessName(app.PackageName)) - if err != nil { - log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get source %s", c.HandlerName(), app.PackageName)) - c.JSON(http.StatusInternalServerError, - server.HttpResponse{ - Code: code.GetErrorCode(err), - Message: err.Error(), - }, - ) - c.Abort() - return - } - - c.JSON(http.StatusOK, server.HttpResponse{Result: source}) -} - -func loginHandler(c *gin.Context) { - dExt, err := getContextDriver(c) - if err != nil { - return - } - - var loginReq LoginRequest - if err := c.ShouldBindJSON(&loginReq); err != nil { - log.Error().Err(err).Msg("validate request failed") - c.JSON(http.StatusBadRequest, server.HttpResponse{ - Code: code.GetErrorCode(code.InvalidParamError), - Message: fmt.Sprintf("validate request param failed: %s", err.Error()), - }) - c.Abort() - return - } - - var info driver_ext.AppLoginInfo - platform := c.Param("platform") - if platform == "android" { - info, err = dExt.(*driver_ext.ShootsAndroidDriver).LoginNoneUI( - loginReq.PackageName, loginReq.PhoneNumber, loginReq.Captcha, loginReq.Password) - } else { - // ios - info, err = dExt.(*driver_ext.ShootsIOSDriver).LoginNoneUI( - loginReq.PackageName, loginReq.PhoneNumber, loginReq.Captcha, loginReq.Password) - } - if err != nil { - log.Err(err).Msg(fmt.Sprintf("[%s]: failed to login", c.HandlerName())) - c.JSON(http.StatusInternalServerError, - server.HttpResponse{ - Code: code.GetErrorCode(err), - Message: err.Error(), - }, - ) - c.Abort() - return - } - c.JSON(http.StatusOK, server.HttpResponse{Code: 0, Message: "success", Result: info}) -} - -func logoutHandler(c *gin.Context) { - dExt, err := getContextDriver(c) - if err != nil { - return - } - - var logoutReq LogoutRequest - if err := c.ShouldBindJSON(&logoutReq); err != nil { - log.Error().Err(err).Msg("validate request failed") - c.JSON(http.StatusBadRequest, server.HttpResponse{ - Code: code.GetErrorCode(code.InvalidParamError), - Message: fmt.Sprintf("validate request param failed: %s", err.Error()), - }) - c.Abort() - return - } - - platform := c.Param("platform") - if platform == "android" { - err = dExt.(*driver_ext.ShootsAndroidDriver).LogoutNoneUI(logoutReq.PackageName) - } else { - // ios - err = dExt.(*driver_ext.ShootsIOSDriver).LogoutNoneUI(logoutReq.PackageName) - } - if err != nil { - log.Err(err).Msg(fmt.Sprintf("[%s]: failed to login", c.HandlerName())) - c.JSON(http.StatusInternalServerError, - server.HttpResponse{ - Code: code.GetErrorCode(err), - Message: err.Error(), - }, - ) - c.Abort() - return - } - c.JSON(http.StatusOK, server.HttpResponse{Code: 0, Message: "success"}) -} - -type LoginRequest struct { - PackageName string `json:"packageName"` - PhoneNumber string `json:"phoneNumber"` - Captcha string `json:"captcha"` - Password string `json:"password"` -} - -type LogoutRequest struct { - PackageName string `json:"packageName"` -} - -var uiClients = make(map[string]uixt.IDriver) // UI automation clients for iOS and Android, key is udid/serial - -func handleDeviceContext() gin.HandlerFunc { - return func(c *gin.Context) { - platform := c.Param("platform") - serial := c.Param("serial") - if serial == "" { - log.Error().Str("platform", platform).Msg(fmt.Sprintf("[%s]: serial is empty", c.HandlerName())) - c.JSON(http.StatusBadRequest, server.HttpResponse{ - Code: code.GetErrorCode(code.InvalidParamError), - Message: "serial is empty", - }) - c.Abort() - return - } - // get cached driver - if driver, ok := uiClients[serial]; ok { - c.Set("driver", driver) - c.Next() - return - } - - switch strings.ToLower(platform) { - case "android": - device, err := uixt.NewAndroidDevice( - option.WithSerialNumber(serial)) - if err != nil { - log.Error().Err(err).Str("platform", platform).Str("serial", serial). - Msg("android device not found") - c.JSON(http.StatusBadRequest, - server.HttpResponse{ - Code: code.GetErrorCode(err), - Message: err.Error(), - }, - ) - c.Abort() - return - } - - driver, err := driver_ext.NewShootsAndroidDriver(device) - if err != nil { - log.Error().Err(err).Str("platform", platform).Str("serial", serial). - Msg("failed to init shoots android driver") - c.JSON(http.StatusInternalServerError, - server.HttpResponse{ - Code: code.GetErrorCode(err), - Message: err.Error(), - }, - ) - c.Abort() - return - } - c.Set("driver", driver) - // cache driver - uiClients[serial] = driver - default: - c.JSON(http.StatusBadRequest, server.HttpResponse{ - Code: code.GetErrorCode(code.InvalidParamError), - Message: fmt.Sprintf("unsupported platform %s", platform), - }) - c.Abort() - return - } - c.Next() - } -} - -func getContextDriver(c *gin.Context) (uixt.IDriver, error) { - driverObj, exists := c.Get("driver") - if !exists { - log.Error().Msg("init device driver failed") - c.JSON(http.StatusInternalServerError, - server.HttpResponse{ - Code: code.GetErrorCode(code.MobileUIDriverError), - Message: "init driver failed", - }, - ) - c.Abort() - return nil, fmt.Errorf("driver not found") - } - dExt := driverObj.(uixt.IDriver) - return dExt, nil -} diff --git a/server/source.go b/server/source.go index b72e69bc..fd184f12 100644 --- a/server/source.go +++ b/server/source.go @@ -2,12 +2,10 @@ package server import ( "encoding/base64" - "net/http" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" - "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) @@ -45,13 +43,7 @@ func screenResultHandler(c *gin.Context) { screenResult, err := dExt.GetScreenResult(actionOptions...) if err != nil { log.Err(err).Msg("get screen result failed") - c.JSON(http.StatusInternalServerError, - HttpResponse{ - Code: code.GetErrorCode(err), - Message: err.Error(), - }, - ) - c.Abort() + RenderError(c, err) return } RenderSuccess(c, screenResult) diff --git a/server/uixt.go b/server/uixt.go index c2dccbe9..c40d430e 100644 --- a/server/uixt.go +++ b/server/uixt.go @@ -1,10 +1,7 @@ package server import ( - "net/http" - "github.com/gin-gonic/gin" - "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/pkg/uixt" "github.com/rs/zerolog/log" ) @@ -25,13 +22,7 @@ func uixtActionHandler(c *gin.Context) { if err = dExt.DoAction(req); err != nil { log.Err(err).Interface("action", req). Msg("exec uixt action failed") - c.JSON(http.StatusInternalServerError, - HttpResponse{ - Code: code.GetErrorCode(err), - Message: err.Error(), - }, - ) - c.Abort() + RenderError(c, err) return } RenderSuccess(c, true) @@ -54,13 +45,7 @@ func uixtActionsHandler(c *gin.Context) { if err = dExt.DoAction(action); err != nil { log.Err(err).Interface("action", action). Msg("exec uixt action failed") - c.JSON(http.StatusInternalServerError, - HttpResponse{ - Code: code.GetErrorCode(err), - Message: err.Error(), - }, - ) - c.Abort() + RenderError(c, err) return } }