diff --git a/hrp/cmd/adb/install.go b/hrp/cmd/adb/install.go new file mode 100644 index 00000000..b7245c81 --- /dev/null +++ b/hrp/cmd/adb/install.go @@ -0,0 +1,62 @@ +package adb + +import ( + "fmt" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/sdk" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + +var installCmd = &cobra.Command{ + Use: "install [flags] PACKAGE", + Short: "Push package to the device and install them atomically", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + startTime := time.Now() + defer func() { + sdk.SendGA4Event("hrp_adb_devices", map[string]interface{}{ + "args": strings.Join(args, "-"), + "success": err == nil, + "engagement_time_msec": time.Since(startTime).Milliseconds(), + }) + }() + _, err = getDevice(serial) + if err != nil { + return err + } + + device, err := uixt.NewAndroidDevice(uixt.WithSerialNumber(serial)) + if err != nil { + fmt.Println(err) + return err + } + driverExt, err := device.NewDriver() + if err != nil { + fmt.Println(err) + return err + } + replace, _ := cmd.Flags().GetBool("replace") + downgrade, _ := cmd.Flags().GetBool("downgrade") + grant, _ := cmd.Flags().GetBool("grant") + option := uixt.InstallOptions{Reinstall: replace, GrantPermission: grant, Downgrade: downgrade} + err = driverExt.Install(args[0], option) + if err != nil { + fmt.Println(err) + return err + } + fmt.Println("success") + return nil + }, +} + +func init() { + installCmd.Flags().StringVarP(&serial, "serial", "s", "", "filter by device's serial") + installCmd.Flags().BoolP("replace", "r", false, "replace existing application") + installCmd.Flags().BoolP("downgrade", "d", false, "allow version code downgrade (debuggable packages only)") + installCmd.Flags().BoolP("grant", "g", false, "grant all runtime permissions") + androidRootCmd.AddCommand(installCmd) +} diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 686bba9a..17fed9e6 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.4.0 \ No newline at end of file +v4.5.0.20240425 \ No newline at end of file diff --git a/hrp/pkg/uixt/android_device.go b/hrp/pkg/uixt/android_device.go index b154a405..eef59e79 100644 --- a/hrp/pkg/uixt/android_device.go +++ b/hrp/pkg/uixt/android_device.go @@ -4,26 +4,36 @@ import ( "bufio" "bytes" "context" + "crypto/md5" + "encoding/base64" + "encoding/hex" "fmt" "io" "net" "os/exec" + "regexp" + "strconv" "strings" + "time" "github.com/httprunner/funplugin/myexec" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/json" "github.com/httprunner/httprunner/v4/hrp/pkg/gadb" ) var ( - AdbServerHost = "localhost" - AdbServerPort = gadb.AdbServerPort // 5037 - UIA2ServerHost = "localhost" - UIA2ServerPort = 6790 + AdbServerHost = "localhost" + AdbServerPort = gadb.AdbServerPort // 5037 + UIA2ServerHost = "localhost" + UIA2ServerPort = 6790 + DeviceTempPath = "/data/local/tmp" + EvalInstallerPackageName = "sogou.mobile.explorer" + InstallViaInstallerCommand = "am start -S -n sogou.mobile.explorer/.PackageInstallerActivity -d" ) const forwardToPrefix = "forward-to-" @@ -263,6 +273,118 @@ func (dev *AndroidDevice) StopPcap() string { return "" } +func (dev *AndroidDevice) Install(app io.ReadSeeker, opts InstallOptions) error { + brand, err := dev.d.Brand() + if err != nil { + return err + } + args := []string{} + if opts.Reinstall { + args = append(args, "-r") + } + if opts.GrantPermission { + args = append(args, "-g") + } + if opts.Downgrade { + args = append(args, "-d") + } + switch strings.ToLower(brand) { + case "vivo": + return dev.installVivoSilent(app, args...) + case "oppo", "realme", "oneplus": + if dev.d.IsPackagesInstalled(EvalInstallerPackageName) { + return dev.installViaInstaller(app, args...) + } + log.Warn().Msg("oppo not install eval installer") + return dev.installCommon(app, args...) + default: + return dev.installCommon(app, args...) + } +} + +func (dev *AndroidDevice) installVivoSilent(app io.ReadSeeker, args ...string) error { + currentTime := builtin.GetCurrentDay() + md5HashInBytes := md5.Sum([]byte(currentTime)) + verifyCode := hex.EncodeToString(md5HashInBytes[:]) + verifyCode = base64.StdEncoding.EncodeToString([]byte(verifyCode)) + verifyCode = verifyCode[:8] + verifyCode = "-V" + verifyCode + args = append([]string{verifyCode}, args...) + _, err := dev.d.InstallAPK(app, args...) + return err +} + +func (dev *AndroidDevice) installViaInstaller(app io.ReadSeeker, args ...string) error { + appRemotePath := "/data/local/tmp/" + strconv.FormatInt(time.Now().UnixMilli(), 10) + ".apk" + err := dev.d.Push(app, appRemotePath, time.Now()) + if err != nil { + return err + } + quit := make(chan struct{}) + done := make(chan error) + defer func() { close(quit) }() + // 需要监听是否完成安装 + go func() { + logcat := NewAdbLogcat(dev.d.Serial()) + err = logcat.CatchLogcat("PackageInstallerCallback") + if err != nil { + done <- err + return + } + scanner := bufio.NewScanner(logcat.reader) + defer func() { + close(done) + _ = logcat.Stop() + }() + for scanner.Scan() { + select { + case <-quit: + break + default: + line := scanner.Text() + re := regexp.MustCompile(`\{.*?}`) + match := re.FindString(line) + if match == "" { + continue + } + var result InstallResult + err := json.Unmarshal([]byte(match), &result) + if err != nil { + log.Warn().Msg("parse Install msg line error: " + match) + continue + } + if result.Result == 0 { + // 安装成功 + done <- nil + return + } else { + done <- errors.New(match) + } + } + } + done <- errors.New("install failed by installer") + }() + args = strings.Split(InstallViaInstallerCommand, " ") + args = append(args, appRemotePath) + _, err = dev.d.RunShellCommand("am", args[1:]...) + if err != nil { + return err + } + // 等待安装完成或超时 + timeout := 1 * time.Minute + select { + case err := <-done: + return err // 返回安装结果或错误 + case <-time.After(timeout): + return fmt.Errorf("installation timed out after %v", timeout) + } +} + +func (dev *AndroidDevice) installCommon(app io.ReadSeeker, args ...string) error { + _, err := dev.d.InstallAPK(app, args...) + return err +} + func getFreePort() (int, error) { addr, err := net.ResolveTCPAddr("tcp", "localhost:0") if err != nil { diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index b9ad0041..2a8e6d97 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -50,6 +50,18 @@ func WithThreshold(threshold float64) CVOption { } } +type InstallOptions struct { + Reinstall bool + GrantPermission bool + Downgrade bool +} + +type InstallResult struct { + Result int `json:"result"` + ErrorCode int `json:"errorCode"` + ErrorMsg string `json:"errorMsg"` +} + type ScreenResult struct { bufSource *bytes.Buffer // raw image buffer bytes imagePath string // image file path @@ -194,6 +206,37 @@ func newDriverExt(device Device, driver WebDriver, plugin funplugin.IPlugin) (dE return dExt, nil } +func (dExt *DriverExt) Install(filePath string, opts InstallOptions) error { + app, err := os.Open(filePath) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("install %s open file failed", filePath)) + } + stopChan := make(chan struct{}) + go func() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + actions := []TapTextAction{ + {Text: "^.*无视风险安装$", Options: []ActionOption{WithTapOffset(100, 0), WithRegex(true), WithIgnoreNotFoundError(true)}}, + {Text: "^已了解此应用未经检测.*", Options: []ActionOption{WithTapOffset(-450, 0), WithRegex(true), WithIgnoreNotFoundError(true)}}, + } + _ = dExt.Driver.TapByTexts(actions...) + _ = dExt.TapByOCR("^(.*无视风险安装|确定|继续|完成|点击继续安装|继续安装旧版本|替换|安装|授权本次安装|继续安装|重新安装)$", WithRegex(true), WithIgnoreNotFoundError(true)) + case <-stopChan: + fmt.Println("Ticker stopped") + return + } + } + }() + defer func() { + close(stopChan) + }() + return dExt.Device.Install(app, opts) +} + // takeScreenShot takes screenshot and saves image file to $CWD/screenshots/ folder func (dExt *DriverExt) takeScreenShot(fileName string) (raw *bytes.Buffer, path string, err error) { // iOS 优先使用 MJPEG 流进行截图,性能最优 diff --git a/hrp/pkg/uixt/interface.go b/hrp/pkg/uixt/interface.go index 3fccf5ba..cf0a0c6f 100644 --- a/hrp/pkg/uixt/interface.go +++ b/hrp/pkg/uixt/interface.go @@ -2,6 +2,7 @@ package uixt import ( "bytes" + "io" "math" "strings" "time" @@ -477,6 +478,8 @@ type Device interface { StartPcap() error StopPcap() string + + Install(app io.ReadSeeker, opts InstallOptions) error } type ForegroundApp struct { diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index b5283667..6cfc4b57 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -466,6 +466,10 @@ func (dev *IOSDevice) StopPcap() string { return dev.pcapFile } +func (dev *IOSDevice) Install(app io.ReadSeeker, opts InstallOptions) error { + return errors.New("install method not implemented") +} + func (dev *IOSDevice) forward(localPort, remotePort int) error { log.Info().Int("localPort", localPort).Int("remotePort", remotePort). Str("udid", dev.UDID).Msg("forward tcp port")