新增模块
This commit is contained in:
226
libs/repuest.go
Normal file
226
libs/repuest.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package libs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------- 用于适配的外部接口/类型 ----------
|
||||||
|
type AdbDevice interface {
|
||||||
|
// CreateConnection 建立到设备的 TCP 连接(通常通过 adb 隧道/port-forward 实现)
|
||||||
|
// network 常为 "tcp" 或 "tcp4"/"tcp6", port 为设备上服务监听端口
|
||||||
|
CreateConnection(network string, port int) (net.Conn, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- HTTPResponse 等价类型 ----------
|
||||||
|
type HTTPResponse struct {
|
||||||
|
Content []byte
|
||||||
|
Status int
|
||||||
|
Reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HTTPResponse) JSON(v interface{}) error {
|
||||||
|
return json.Unmarshal(r.Content, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HTTPResponse) Text() string {
|
||||||
|
return string(r.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 自定义错误类型 ----------
|
||||||
|
var (
|
||||||
|
ErrHTTPTimeout = errors.New("http request timeout")
|
||||||
|
ErrHTTPFailed = errors.New("http request failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------- AdbHTTPConnection 核心:使用 net.Conn 写请求并用 http.ReadResponse 解析 ----------
|
||||||
|
type AdbHTTPConnection struct {
|
||||||
|
Conn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdbHTTPConnection(dev AdbDevice, port int, timeout time.Duration) (*AdbHTTPConnection, error) {
|
||||||
|
// 这里 network 使用 "tcp",调用方可依据实际实现调整
|
||||||
|
conn, err := dev.CreateConnection("tcp", port)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to connect to uiautomator2 server: %w", err)
|
||||||
|
}
|
||||||
|
// 设置默认 deadline,调用方可在需要时调整 Conn.SetDeadline
|
||||||
|
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||||
|
return &AdbHTTPConnection{Conn: conn}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AdbHTTPConnection) Close() error {
|
||||||
|
if c.Conn != nil {
|
||||||
|
return c.Conn.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendRequest 写入 HTTP 请求并返回 *http.Response
|
||||||
|
func (c *AdbHTTPConnection) sendRequest(req *http.Request, timeout time.Duration) (*http.Response, error) {
|
||||||
|
// 确保 deadline
|
||||||
|
if timeout > 0 {
|
||||||
|
_ = c.Conn.SetDeadline(time.Now().Add(timeout))
|
||||||
|
} else {
|
||||||
|
_ = c.Conn.SetDeadline(time.Time{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 http.Request 序列化为原始 HTTP 报文并写到 conn
|
||||||
|
var buf bytes.Buffer
|
||||||
|
// 行: METHOD PATH HTTP/1.1
|
||||||
|
path := req.URL.RequestURI()
|
||||||
|
if path == "" {
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&buf, "%s %s HTTP/1.1\r\n", req.Method, path)
|
||||||
|
// Host 头;Python 里使用 "localhost" 但最终是通过 adb 隧道,Host 不重要,这里写 localhost
|
||||||
|
fmt.Fprintf(&buf, "Host: localhost\r\n")
|
||||||
|
|
||||||
|
// 写 headers
|
||||||
|
// 保证 Content-Length 或 Transfer-Encoding 存在
|
||||||
|
if req.Header.Get("User-Agent") == "" {
|
||||||
|
req.Header.Set("User-Agent", "uiautomator2")
|
||||||
|
}
|
||||||
|
if req.Header.Get("Accept-Encoding") == "" {
|
||||||
|
// 与 Python 保持一致,明确禁用压缩
|
||||||
|
req.Header.Set("Accept-Encoding", "")
|
||||||
|
}
|
||||||
|
if req.Header.Get("Content-Type") == "" {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy headers into buffer
|
||||||
|
for k, vals := range req.Header {
|
||||||
|
for _, v := range vals {
|
||||||
|
fmt.Fprintf(&buf, "%s: %s\r\n", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// body
|
||||||
|
var bodyBytes []byte
|
||||||
|
if req.Body != nil {
|
||||||
|
var err error
|
||||||
|
bodyBytes, err = io.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read request body failed: %w", err)
|
||||||
|
}
|
||||||
|
// set Content-Length
|
||||||
|
fmt.Fprintf(&buf, "Content-Length: %d\r\n", len(bodyBytes))
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&buf, "Content-Length: 0\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// header-body separator
|
||||||
|
buf.WriteString("\r\n")
|
||||||
|
|
||||||
|
// write header+body to conn
|
||||||
|
if _, err := c.Conn.Write(buf.Bytes()); err != nil {
|
||||||
|
return nil, fmt.Errorf("write request headers failed: %w", err)
|
||||||
|
}
|
||||||
|
if len(bodyBytes) > 0 {
|
||||||
|
if _, err := c.Conn.Write(bodyBytes); err != nil {
|
||||||
|
return nil, fmt.Errorf("write request body failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read response using http.ReadResponse
|
||||||
|
reader := bufio.NewReader(c.Conn)
|
||||||
|
// note: http.ReadResponse expects a Request to be passed for RequestURI related logic,
|
||||||
|
// but if nil it still parses status & headers fine. Provide req for completeness.
|
||||||
|
resp, err := http.ReadResponse(reader, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read http response failed: %w", err)
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- _http_request 的 Go 等价实现 ----------
|
||||||
|
func HttpRequest(ctx context.Context, dev AdbDevice, devicePort int, method, path string, data map[string]interface{}, timeoutSecs float64, printRequest bool) (*HTTPResponse, error) {
|
||||||
|
// 兼容 python 默认 timeout
|
||||||
|
if timeoutSecs <= 0 {
|
||||||
|
timeoutSecs = 10.0
|
||||||
|
}
|
||||||
|
timeout := time.Duration(timeoutSecs * float64(time.Second))
|
||||||
|
|
||||||
|
// debug 打印 curl 样式
|
||||||
|
if printRequest {
|
||||||
|
now := time.Now().Format("15:04:05.000")
|
||||||
|
url := fmt.Sprintf("http://127.0.0.1:%d%s", devicePort, path)
|
||||||
|
if data != nil {
|
||||||
|
b, _ := json.Marshal(data)
|
||||||
|
fmt.Printf("# http timeout=%.3f\n%s $ curl -X %s %s -d '%s'\n", timeoutSecs, now, method, url, string(b))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("# http timeout=%.3f\n%s $ curl -X %s %s\n", timeoutSecs, now, method, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造 http.Request
|
||||||
|
var body io.Reader
|
||||||
|
if data != nil {
|
||||||
|
b, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("json marshal data failed: %w", err)
|
||||||
|
}
|
||||||
|
body = bytes.NewReader(b)
|
||||||
|
}
|
||||||
|
// URL 可以是任何合法的虚拟 URL,因为我们直接写原始请求行到 conn
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, "http://localhost"+path, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create http request failed: %w", err)
|
||||||
|
}
|
||||||
|
// 设置 headers 与 Python 保持一致
|
||||||
|
req.Header.Set("User-Agent", "uiautomator2")
|
||||||
|
req.Header.Set("Accept-Encoding", "")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// 建立到设备的连接(AdbHTTPConnection.connect)
|
||||||
|
connWrapper, err := NewAdbHTTPConnection(dev, devicePort, timeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer connWrapper.Close()
|
||||||
|
|
||||||
|
// 发送请求并读取响应
|
||||||
|
resp, err := connWrapper.sendRequest(req, timeout)
|
||||||
|
if err != nil {
|
||||||
|
// 判断是否为超时(net.Error with Timeout)
|
||||||
|
var netErr net.Error
|
||||||
|
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrHTTPTimeout, err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrHTTPFailed, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 按块读取响应体(与 Python 的循环等效)
|
||||||
|
content, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("http request failed: %d %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &HTTPResponse{
|
||||||
|
Content: content,
|
||||||
|
Status: resp.StatusCode,
|
||||||
|
Reason: resp.Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
if printRequest {
|
||||||
|
now := time.Now().Format("15:04:05.000")
|
||||||
|
fmt.Printf("%s Response >>>\n%s\n<<< END timed_used = %.3f\n\n", now, strings.TrimRight(response.Text(), "\n"), time.Since(time.Now().Add(-timeout)).Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package selector
|
package libs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|||||||
Reference in New Issue
Block a user