mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-21 16:23:16 +08:00
feat: 免密自动化安装
This commit is contained in:
62
hrp/cmd/adb/install.go
Normal file
62
hrp/cmd/adb/install.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
v4.4.0
|
v4.5.0.20240425
|
||||||
@@ -4,26 +4,36 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/httprunner/funplugin/myexec"
|
"github.com/httprunner/funplugin/myexec"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"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/code"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
AdbServerHost = "localhost"
|
AdbServerHost = "localhost"
|
||||||
AdbServerPort = gadb.AdbServerPort // 5037
|
AdbServerPort = gadb.AdbServerPort // 5037
|
||||||
UIA2ServerHost = "localhost"
|
UIA2ServerHost = "localhost"
|
||||||
UIA2ServerPort = 6790
|
UIA2ServerPort = 6790
|
||||||
|
DeviceTempPath = "/data/local/tmp"
|
||||||
|
EvalInstallerPackageName = "sogou.mobile.explorer"
|
||||||
|
InstallViaInstallerCommand = "am start -S -n sogou.mobile.explorer/.PackageInstallerActivity -d"
|
||||||
)
|
)
|
||||||
|
|
||||||
const forwardToPrefix = "forward-to-"
|
const forwardToPrefix = "forward-to-"
|
||||||
@@ -263,6 +273,118 @@ func (dev *AndroidDevice) StopPcap() string {
|
|||||||
return ""
|
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) {
|
func getFreePort() (int, error) {
|
||||||
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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 {
|
type ScreenResult struct {
|
||||||
bufSource *bytes.Buffer // raw image buffer bytes
|
bufSource *bytes.Buffer // raw image buffer bytes
|
||||||
imagePath string // image file path
|
imagePath string // image file path
|
||||||
@@ -194,6 +206,37 @@ func newDriverExt(device Device, driver WebDriver, plugin funplugin.IPlugin) (dE
|
|||||||
return dExt, nil
|
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
|
// takeScreenShot takes screenshot and saves image file to $CWD/screenshots/ folder
|
||||||
func (dExt *DriverExt) takeScreenShot(fileName string) (raw *bytes.Buffer, path string, err error) {
|
func (dExt *DriverExt) takeScreenShot(fileName string) (raw *bytes.Buffer, path string, err error) {
|
||||||
// iOS 优先使用 MJPEG 流进行截图,性能最优
|
// iOS 优先使用 MJPEG 流进行截图,性能最优
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package uixt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -477,6 +478,8 @@ type Device interface {
|
|||||||
|
|
||||||
StartPcap() error
|
StartPcap() error
|
||||||
StopPcap() string
|
StopPcap() string
|
||||||
|
|
||||||
|
Install(app io.ReadSeeker, opts InstallOptions) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForegroundApp struct {
|
type ForegroundApp struct {
|
||||||
|
|||||||
@@ -466,6 +466,10 @@ func (dev *IOSDevice) StopPcap() string {
|
|||||||
return dev.pcapFile
|
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 {
|
func (dev *IOSDevice) forward(localPort, remotePort int) error {
|
||||||
log.Info().Int("localPort", localPort).Int("remotePort", remotePort).
|
log.Info().Int("localPort", localPort).Int("remotePort", remotePort).
|
||||||
Str("udid", dev.UDID).Msg("forward tcp port")
|
Str("udid", dev.UDID).Msg("forward tcp port")
|
||||||
|
|||||||
Reference in New Issue
Block a user