diff --git a/libs/repuest.go b/libs/repuest.go new file mode 100644 index 0000000..5b471fc --- /dev/null +++ b/libs/repuest.go @@ -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 +} diff --git a/libs/selector.go b/libs/selector.go index c5c6b06..ccc0ab5 100644 --- a/libs/selector.go +++ b/libs/selector.go @@ -1,4 +1,4 @@ -package selector +package libs import ( "encoding/json"