14 // Client interacts with UDP trackers via its Writer and Dispatcher. It has no knowledge of
15 // connection specifics.
19 connIdIssued time.Time
20 Dispatcher *Dispatcher
24 func (cl *Client) Announce(
25 ctx context.Context, req AnnounceRequest, peers AnnounceResponsePeers, opts Options,
27 respHdr AnnounceResponseHeader, err error,
29 respBody, err := cl.request(ctx, ActionAnnounce, append(mustMarshal(req), opts.Encode()...))
33 r := bytes.NewBuffer(respBody)
34 err = Read(r, &respHdr)
36 err = fmt.Errorf("reading response header: %w", err)
39 err = peers.UnmarshalBinary(r.Bytes())
41 err = fmt.Errorf("reading response peers: %w", err)
46 func (cl *Client) Scrape(
47 ctx context.Context, ihs []InfoHash,
49 out ScrapeResponse, err error,
51 // There's no way to pass options in a scrape, since we don't when the request body ends.
52 respBody, err := cl.request(ctx, ActionScrape, mustMarshal(ScrapeRequest(ihs)))
56 r := bytes.NewBuffer(respBody)
58 var item ScrapeInfohashResult
63 out = append(out, item)
65 if len(out) > len(ihs) {
66 err = fmt.Errorf("got %v results but expected %v", len(out), len(ihs))
72 func (cl *Client) connect(ctx context.Context) (err error) {
73 // We could get fancier here and use RWMutex, and even fire off the connection asynchronously
74 // and provide a grace period while it resolves.
77 if !cl.connIdIssued.IsZero() && time.Since(cl.connIdIssued) < time.Minute {
80 respBody, err := cl.request(ctx, ActionConnect, nil)
84 var connResp ConnectionResponse
85 err = binary.Read(bytes.NewReader(respBody), binary.BigEndian, &connResp)
89 cl.connId = connResp.ConnectionId
90 cl.connIdIssued = time.Now()
94 func (cl *Client) connIdForRequest(ctx context.Context, action Action) (id ConnectionId, err error) {
95 if action == ActionConnect {
96 id = ConnectRequestConnectionId
107 func (cl *Client) requestWriter(ctx context.Context, action Action, body []byte, tId TransactionId) (err error) {
110 var connId ConnectionId
111 connId, err = cl.connIdForRequest(ctx, action)
116 err = binary.Write(&buf, binary.BigEndian, RequestHeader{
117 ConnectionId: connId,
125 _, err = cl.Writer.Write(buf.Bytes())
132 case <-time.After(timeout(n)):
137 func (cl *Client) request(ctx context.Context, action Action, body []byte) (respBody []byte, err error) {
138 respChan := make(chan DispatchedResponse, 1)
139 t := cl.Dispatcher.NewTransaction(func(dr DispatchedResponse) {
143 ctx, cancel := context.WithCancel(ctx)
145 writeErr := make(chan error, 1)
147 writeErr <- cl.requestWriter(ctx, action, body, t.Id())
150 case dr := <-respChan:
151 if dr.Header.Action == action {
153 } else if dr.Header.Action == ActionError {
154 err = errors.New(string(dr.Body))
156 err = fmt.Errorf("unexpected response action %v", dr.Header.Action)
158 case err = <-writeErr:
159 err = fmt.Errorf("write error: %w", err)