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 a67821a0..a61f96e3 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.6.2 \ No newline at end of file +v4.6.2 diff --git a/hrp/pkg/gadb/client.go b/hrp/pkg/gadb/client.go index eb6b9d8c..a81ac116 100644 --- a/hrp/pkg/gadb/client.go +++ b/hrp/pkg/gadb/client.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" "strings" + "time" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -209,8 +210,8 @@ func (c Client) KillServer() (err error) { return } -func (c Client) createTransport() (tp transport, err error) { - return newTransport(fmt.Sprintf("%s:%d", c.host, c.port)) +func (c Client) createTransport(readTimeout ...time.Duration) (tp transport, err error) { + return newTransport(fmt.Sprintf("%s:%d", c.host, c.port), readTimeout...) } func (c Client) executeCommand(command string, onlyVerifyResponse ...bool) (resp string, err error) { diff --git a/hrp/pkg/gadb/device.go b/hrp/pkg/gadb/device.go index bb530249..882f42bc 100644 --- a/hrp/pkg/gadb/device.go +++ b/hrp/pkg/gadb/device.go @@ -446,8 +446,8 @@ func (d *Device) EnableAdbOverTCP(port ...int) (err error) { return } -func (d *Device) createDeviceTransport() (tp transport, err error) { - if tp, err = newTransport(fmt.Sprintf("%s:%d", d.adbClient.host, d.adbClient.port)); err != nil { +func (d *Device) createDeviceTransport(readTimeout ...time.Duration) (tp transport, err error) { + if tp, err = newTransport(fmt.Sprintf("%s:%d", d.adbClient.host, d.adbClient.port), readTimeout...); err != nil { return transport{}, err } @@ -586,7 +586,7 @@ func (d *Device) installViaABBExec(apk io.ReadSeeker, args ...string) (raw []byt if err != nil { return nil, err } - if tp, err = d.createDeviceTransport(); err != nil { + if tp, err = d.createDeviceTransport(5 * time.Minute); err != nil { return nil, err } defer func() { _ = tp.Close() }() diff --git a/hrp/pkg/gadb/transport.go b/hrp/pkg/gadb/transport.go index c55b32b7..d09b5c72 100644 --- a/hrp/pkg/gadb/transport.go +++ b/hrp/pkg/gadb/transport.go @@ -17,7 +17,7 @@ import ( var ErrConnBroken = errors.New("socket connection broken") -var DefaultAdbReadTimeout time.Duration = 60 +var DefaultAdbReadTimeout time.Duration = 300 var regexDeviceOffline = regexp.MustCompile("device .* not found") diff --git a/hrp/pkg/uixt/android_device.go b/hrp/pkg/uixt/android_device.go index 217e9bcd..44fbb679 100644 --- a/hrp/pkg/uixt/android_device.go +++ b/hrp/pkg/uixt/android_device.go @@ -4,9 +4,16 @@ import ( "bufio" "bytes" "context" + "crypto/md5" "embed" + "encoding/base64" + "encoding/hex" "fmt" + "io" + "net" "os/exec" + "regexp" + "strconv" "strings" "time" @@ -14,6 +21,7 @@ import ( "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/env" "github.com/httprunner/httprunner/v4/hrp/internal/json" @@ -21,11 +29,13 @@ import ( ) var ( - AdbServerHost = "localhost" - AdbServerPort = gadb.AdbServerPort // 5037 - UIA2ServerHost = "localhost" - UIA2ServerPort = 6790 - DouyinServerPort = 32316 + DouyinServerPort = 32316 + AdbServerHost = "localhost" + AdbServerPort = gadb.AdbServerPort // 5037 + UIA2ServerHost = "localhost" + UIA2ServerPort = 6790 + EvalInstallerPackageName = "sogou.mobile.explorer" + InstallViaInstallerCommand = "am start -S -n sogou.mobile.explorer/.PackageInstallerActivity -d" ) //go:embed eval_tool @@ -318,6 +328,120 @@ 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 + } + done := make(chan error) + defer func() { + close(done) + }() + logcat := NewAdbLogcatWithCallback(dev.d.Serial(), func(line string) { + re := regexp.MustCompile(`\{.*?}`) + match := re.FindString(line) + if match == "" { + return + } + var result InstallResult + err := json.Unmarshal([]byte(match), &result) + if err != nil { + log.Warn().Msg("parse Install msg line error: " + match) + return + } + if result.Result == 0 { + // 安装成功 + done <- nil + } else { + done <- errors.New(match) + } + }) + err = logcat.CatchLogcat("PackageInstallerCallback") + if err != nil { + return err + } + defer func() { + _ = logcat.Stop() + }() + + // 需要监听是否完成安装 + args = strings.Split(InstallViaInstallerCommand, " ") + args = append(args, appRemotePath) + _, err = dev.d.RunShellCommand("am", args[1:]...) + if err != nil { + return err + } + // 等待安装完成或超时 + timeout := 3 * 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 { + return 0, errors.Wrap(err, "resolve tcp addr failed") + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, errors.Wrap(err, "listen tcp addr failed") + } + defer func() { _ = l.Close() }() + return l.Addr().(*net.TCPAddr).Port, nil +} + type LineCallback func(string) type AdbLogcat struct { diff --git a/hrp/pkg/uixt/android_test.go b/hrp/pkg/uixt/android_test.go index 0dafd3d4..9fe90933 100644 --- a/hrp/pkg/uixt/android_test.go +++ b/hrp/pkg/uixt/android_test.go @@ -3,8 +3,6 @@ package uixt import ( - "encoding/json" - "fmt" "io/ioutil" "os" "strings" @@ -445,8 +443,6 @@ func TestConvertPoints(t *testing.T) { if len(eps) != 3 { t.Fatal() } - jsons, _ := json.Marshal(eps) - println(fmt.Sprintf("%v", string(jsons))) } func TestDriver_ShellInputUnicode(t *testing.T) { diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index dc1a51de..7fe57571 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 @@ -201,6 +213,37 @@ func newDriverExt(device Device, driver WebDriver, options ...DriverOption) (dEx 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 4920cc73..017e52f9 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" @@ -504,6 +505,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 e8a161ca..910bcba7 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -472,6 +472,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")