mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-05 07:26:55 +08:00
change: use context to contral ScreenRecord timeout or cancel
This commit is contained in:
@@ -1 +1 @@
|
|||||||
v5.0.0-beta-2503061758
|
v5.0.0-beta-2503062216
|
||||||
|
|||||||
+29
-6
@@ -2,6 +2,7 @@ package gadb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -761,16 +762,38 @@ func (d *Device) ScreenCap() ([]byte, error) {
|
|||||||
return buffer.Bytes(), nil
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) ScreenRecord(seconds float64) ([]byte, error) {
|
func (d *Device) ScreenRecord(ctx context.Context) ([]byte, error) {
|
||||||
videoPath := fmt.Sprintf("/sdcard/screenrecord_%d.mp4", time.Now().Unix())
|
videoPath := fmt.Sprintf("/sdcard/screenrecord_%d.mp4", time.Now().Unix())
|
||||||
_, err := d.RunShellCommandWithBytes("screenrecord",
|
|
||||||
"--time-limit", fmt.Sprintf("%.1f", seconds), videoPath)
|
done := make(chan error, 1)
|
||||||
if err != nil {
|
go func() {
|
||||||
return nil, errors.Wrap(err, "screenrecord failed")
|
_, err := d.RunShellCommandWithBytes("screenrecord", videoPath)
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// timeout or cancelled
|
||||||
|
pid, err := d.RunShellCommand("pidof", "screenrecord")
|
||||||
|
if err == nil && pid != "" {
|
||||||
|
// 发送 SIGINT 信号终止录屏
|
||||||
|
_, _ = d.RunShellCommand("kill", "-2", strings.TrimSpace(pid))
|
||||||
|
}
|
||||||
|
<-done // 等待进程完全退出
|
||||||
|
case err := <-done:
|
||||||
|
// adb screenrecord will exit on reached 180s
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "screenrecord failed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove temp file
|
||||||
|
defer func() {
|
||||||
|
go d.RunShellCommand("rm", videoPath)
|
||||||
|
}()
|
||||||
|
|
||||||
buffer := bytes.NewBuffer(nil)
|
buffer := bytes.NewBuffer(nil)
|
||||||
err = d.Pull(videoPath, buffer)
|
err := d.Pull(videoPath, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "pull video failed")
|
return nil, errors.Wrap(err, "pull video failed")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package gadb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -295,6 +296,36 @@ func TestDevice_Pull(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDevice_ScreenRecord(t *testing.T) {
|
||||||
|
setupDevices(t)
|
||||||
|
|
||||||
|
for _, dev := range devices {
|
||||||
|
// screen record with time limit 5 seconds
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
if _, err := dev.ScreenRecord(ctx); err != nil {
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dev := range devices {
|
||||||
|
// screen record with cancel signal
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
done := make(chan error)
|
||||||
|
go func() {
|
||||||
|
_, err := dev.ScreenRecord(ctx)
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
// record for 3 seconds
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err := <-done
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDevice_RunShellCommandBackgroundWithBytes(t *testing.T) {
|
func TestDevice_RunShellCommandBackgroundWithBytes(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
adbClient Client
|
adbClient Client
|
||||||
|
|||||||
+25
-19
@@ -791,11 +791,25 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string
|
|||||||
filePath = filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
|
filePath = filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
if options.Context != nil {
|
||||||
|
ctx = options.Context
|
||||||
|
} else {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
var cancel context.CancelFunc
|
||||||
duration := options.ScreenRecordDuration
|
duration := options.ScreenRecordDuration
|
||||||
if duration == 0 {
|
if duration == 0 {
|
||||||
duration = options.Duration
|
duration = options.Duration
|
||||||
}
|
}
|
||||||
audioOn := options.ScreenRecordWithAudio
|
if duration != 0 {
|
||||||
|
ctx, cancel = context.WithTimeout(ctx,
|
||||||
|
time.Duration(duration*float64(time.Second)))
|
||||||
|
} else {
|
||||||
|
ctx, cancel = context.WithCancel(ctx)
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
// get android system version
|
// get android system version
|
||||||
var sysVersion int
|
var sysVersion int
|
||||||
@@ -809,6 +823,7 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string
|
|||||||
}
|
}
|
||||||
|
|
||||||
var useAdbScreenRecord bool
|
var useAdbScreenRecord bool
|
||||||
|
audioOn := options.ScreenRecordWithAudio
|
||||||
if options.ScreenRecordWithScrcpy {
|
if options.ScreenRecordWithScrcpy {
|
||||||
useAdbScreenRecord = false
|
useAdbScreenRecord = false
|
||||||
} else if !audioOn {
|
} else if !audioOn {
|
||||||
@@ -834,7 +849,9 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if useAdbScreenRecord {
|
if useAdbScreenRecord {
|
||||||
res, err := ad.Device.ScreenRecord(duration)
|
// screen record with adb screenrecord
|
||||||
|
// adb screenrecord duration is limited in range [1,180] seconds
|
||||||
|
res, err := ad.Device.ScreenRecord(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "screen record failed")
|
return "", errors.Wrap(err, "screen record failed")
|
||||||
}
|
}
|
||||||
@@ -844,16 +861,8 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string
|
|||||||
return filePath, nil
|
return filePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// screen record with audio
|
// screen record with scrcpy
|
||||||
log.Info().Float64("duration(s)", duration).Msg("screen record with audio, use scrcpy")
|
log.Info().Float64("duration(s)", duration).Msg("screen record with scrcpy")
|
||||||
|
|
||||||
file, err := os.Create(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrap(err, "create screen record file failed")
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = file.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// start scrcpy
|
// start scrcpy
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
@@ -866,15 +875,10 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string
|
|||||||
)
|
)
|
||||||
cmd.Stdout = io.Discard
|
cmd.Stdout = io.Discard
|
||||||
cmd.Stderr = io.Discard
|
cmd.Stderr = io.Discard
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return "", errors.Wrap(err, "start screen record failed")
|
return "", errors.Wrap(err, "start screen record failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(),
|
|
||||||
time.Duration(duration*float64(time.Second)))
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
done := make(chan error, 1)
|
done := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
done <- cmd.Wait()
|
done <- cmd.Wait()
|
||||||
@@ -882,15 +886,17 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// 超时,优雅停止 scrcpy 进程
|
// timeout or cancelled
|
||||||
|
log.Info().Msg("screen recording stopped")
|
||||||
if err := cmd.Process.Signal(syscall.SIGINT); err != nil {
|
if err := cmd.Process.Signal(syscall.SIGINT); err != nil {
|
||||||
log.Error().Err(err).Msg("failed to stop scrcpy process")
|
log.Error().Err(err).Msg("failed to stop scrcpy process")
|
||||||
_ = cmd.Process.Kill() // 强制结束进程
|
_ = cmd.Process.Kill() // 强制结束进程
|
||||||
}
|
}
|
||||||
<-done // 等待进程完全退出
|
<-done // 等待进程完全退出
|
||||||
case err := <-done:
|
case err := <-done:
|
||||||
|
log.Info().Msg("scrcpy exited")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "screen record failed")
|
return "", errors.Wrap(err, "screen record with scrcpy failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package uixt
|
package uixt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -195,12 +196,15 @@ func TestDriver_ADB_ForegroundInfo(t *testing.T) {
|
|||||||
|
|
||||||
func TestDriver_ADB_ScreenRecord(t *testing.T) {
|
func TestDriver_ADB_ScreenRecord(t *testing.T) {
|
||||||
driver := setupADBDriverExt(t)
|
driver := setupADBDriverExt(t)
|
||||||
|
|
||||||
|
// adb screenrecord --time-limit 5
|
||||||
path1, err := driver.ScreenRecord(
|
path1, err := driver.ScreenRecord(
|
||||||
option.WithScreenRecordDuation(5))
|
option.WithScreenRecordDuation(5))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
defer os.Remove(path1)
|
defer os.Remove(path1)
|
||||||
t.Log(path1)
|
t.Log(path1)
|
||||||
|
|
||||||
|
// scrcpy with time limit
|
||||||
path2, err := driver.ScreenRecord(
|
path2, err := driver.ScreenRecord(
|
||||||
option.WithScreenRecordDuation(5),
|
option.WithScreenRecordDuation(5),
|
||||||
option.WithScreenRecordAudio(true),
|
option.WithScreenRecordAudio(true),
|
||||||
@@ -209,6 +213,7 @@ func TestDriver_ADB_ScreenRecord(t *testing.T) {
|
|||||||
defer os.Remove(path2)
|
defer os.Remove(path2)
|
||||||
t.Log(path2)
|
t.Log(path2)
|
||||||
|
|
||||||
|
// scrcpy with time limit
|
||||||
path3, err := driver.ScreenRecord(
|
path3, err := driver.ScreenRecord(
|
||||||
option.WithScreenRecordDuation(5),
|
option.WithScreenRecordDuation(5),
|
||||||
option.WithScreenRecordScrcpy(true),
|
option.WithScreenRecordScrcpy(true),
|
||||||
@@ -216,6 +221,27 @@ func TestDriver_ADB_ScreenRecord(t *testing.T) {
|
|||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
defer os.Remove(path3)
|
defer os.Remove(path3)
|
||||||
t.Log(path3)
|
t.Log(path3)
|
||||||
|
|
||||||
|
// scrcpy with cancel signal
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
done := make(chan error)
|
||||||
|
go func() {
|
||||||
|
path4, err := driver.ScreenRecord(
|
||||||
|
option.WithContext(ctx),
|
||||||
|
option.WithScreenRecordScrcpy(true),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer os.Remove(path4)
|
||||||
|
t.Log(path4)
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
// record for 3 seconds
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err = <-done
|
||||||
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDriver_ADB_Backspace(t *testing.T) {
|
func TestDriver_ADB_Backspace(t *testing.T) {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ActionOptions struct {
|
type ActionOptions struct {
|
||||||
|
Context context.Context
|
||||||
// log
|
// log
|
||||||
Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log
|
Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log
|
||||||
|
|
||||||
@@ -33,6 +35,9 @@ func (o *ActionOptions) Options() []ActionOption {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.Context != nil {
|
||||||
|
options = append(options, WithContext(o.Context))
|
||||||
|
}
|
||||||
if o.Identifier != "" {
|
if o.Identifier != "" {
|
||||||
options = append(options, WithIdentifier(o.Identifier))
|
options = append(options, WithIdentifier(o.Identifier))
|
||||||
}
|
}
|
||||||
@@ -206,6 +211,12 @@ func NewActionOptions(opts ...ActionOption) *ActionOptions {
|
|||||||
|
|
||||||
type ActionOption func(o *ActionOptions)
|
type ActionOption func(o *ActionOptions)
|
||||||
|
|
||||||
|
func WithContext(ctx context.Context) ActionOption {
|
||||||
|
return func(o *ActionOptions) {
|
||||||
|
o.Context = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithCustomOption(key string, value interface{}) ActionOption {
|
func WithCustomOption(key string, value interface{}) ActionOption {
|
||||||
return func(o *ActionOptions) {
|
return func(o *ActionOptions) {
|
||||||
if o.Custom == nil {
|
if o.Custom == nil {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
import "github.com/httprunner/httprunner/v5/uixt/types"
|
import (
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||||
|
)
|
||||||
|
|
||||||
type ScreenOptions struct {
|
type ScreenOptions struct {
|
||||||
ScreenShotOptions
|
ScreenShotOptions
|
||||||
|
|||||||
Reference in New Issue
Block a user