1 %%%-------------------------------------------------------------------
2 %%% File : tracker_delegate.erl
3 %%% Author : Jesper Louis Andersen <jesper.louis.andersen@gmail.com>
4 %%% License : See COPYING
5 %%% Description : Handles communication with the tracker
7 %%% Created : 17 Jul 2007 by Jesper Louis Andersen <jesper.louis.andersen@gmail.com>
8 %%%-------------------------------------------------------------------
9 -module(etorrent_tracker_communication
).
11 -behaviour(gen_server
).
13 -include("etorrent_mnesia_table.hrl").
16 -export([start_link
/5, contact
/1, stopped
/1, completed
/1, started
/1]).
18 %% gen_server callbacks
19 -export([init
/1, handle_call
/3, handle_cast
/2, handle_info
/2,
20 terminate
/2, code_change
/3]).
22 -record(state
, {should_contact_tracker
= false
,
23 queued_message
= none
,
24 %% The hard timer is the time we *must* wait on the tracker.
25 %% soft timer may be overridden if we want to change state.
35 -define(DEFAULT_REQUEST_TIMEOUT
, 180).
36 -define(DEFAULT_CONNECTION_TIMEOUT_INTERVAL
, 1800).
37 -define(DEFAULT_CONNECTION_TIMEOUT_MIN_INTERVAL
, 60).
38 -define(DEFAULT_TRACKER_OVERLOAD_INTERVAL
, 300).
40 %%====================================================================
42 %%====================================================================
43 %%--------------------------------------------------------------------
44 %% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
45 %% Description: Starts the server
46 %%--------------------------------------------------------------------
47 start_link(ControlPid
, Url
, InfoHash
, PeerId
, TorrentId
) ->
48 gen_server:start_link(?MODULE
,
50 Url
, InfoHash
, PeerId
, TorrentId
],
53 %%--------------------------------------------------------------------
54 %% Function: contact(Pid)
55 %% Description: Contact the tracker right now ignoring the soft timeout
56 %% it won't ignore the hard-timeout though.
57 %%--------------------------------------------------------------------
59 gen_server:cast(Pid
, contact
).
61 %%--------------------------------------------------------------------
62 %% Function: stopped(Pid)
63 %% Description: Tell the tracker that we stopped the torrent
64 %%--------------------------------------------------------------------
66 gen_server:cast(Pid
, stopped
).
68 %%--------------------------------------------------------------------
69 %% Function: started(Pid)
70 %% Description: Tell the tracker that we started the torrent
71 %%--------------------------------------------------------------------
73 gen_server:cast(Pid
, started
).
75 %%--------------------------------------------------------------------
76 %% Function: started(Pid)
77 %% Description: Tell the tracker that we completed the torrent
78 %%--------------------------------------------------------------------
80 gen_server:cast(Pid
, completed
).
82 %%====================================================================
83 %% gen_server callbacks
84 %%====================================================================
86 %%--------------------------------------------------------------------
87 %% Function: init(Args) -> {ok, State} |
88 %% {ok, State, Timeout} |
91 %% Description: Initiates the server
92 %%--------------------------------------------------------------------
93 init([ControlPid
, Url
, InfoHash
, PeerId
, TorrentId
]) ->
94 process_flag(trap_exit
, true
),
95 {ok
, HardRef
} = timer:send_after(0, hard_timeout
),
96 {ok
, SoftRef
} = timer:send_after(timer:seconds(?DEFAULT_CONNECTION_TIMEOUT_INTERVAL
),
98 {ok
, #state
{should_contact_tracker
= false
,
99 control_pid
= ControlPid
,
100 torrent_id
= TorrentId
,
102 info_hash
= InfoHash
,
105 soft_timer
= SoftRef
,
106 hard_timer
= HardRef
,
108 queued_message
= started
}}.
110 %%--------------------------------------------------------------------
111 %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
112 %% {reply, Reply, State, Timeout} |
113 %% {noreply, State} |
114 %% {noreply, State, Timeout} |
115 %% {stop, Reason, Reply, State} |
116 %% {stop, Reason, State}
117 %% Description: Handling call messages
118 %%--------------------------------------------------------------------
119 handle_call(_Request
, _From
, State
) ->
121 {reply
, Reply
, State
}.
123 %%--------------------------------------------------------------------
124 %% Function: handle_cast(Msg, State) -> {noreply, State} |
125 %% {noreply, State, Timeout} |
126 %% {stop, Reason, State}
127 %% Description: Handling cast messages
128 %%----------------------p----------------------------------------------
129 handle_cast(Msg
, S
) when S#state
.hard_timer
=:= none
->
130 NS
= contact_tracker(Msg
, S
),
132 handle_cast(started
, S
) when S#state
.queued_message
=:= completed
->
134 handle_cast(started
, S
) when S#state
.queued_message
=:= stopped
->
135 {noreply
, S#state
{queued_message
= started
}};
136 handle_cast(stopped
, S
) ->
137 {noreply
, S#state
{queued_message
= stopped
}};
138 handle_cast(completed
, S
) ->
139 {noreply
, S#state
{queued_message
= completed
}}.
142 %%--------------------------------------------------------------------
143 %% Function: handle_info(Info, State) -> {noreply, State} |
144 %% {noreply, State, Timeout} |
145 %% {stop, Reason, State}
146 %% Description: Handling all non call/cast messages
147 %%--------------------------------------------------------------------
148 handle_info(hard_timeout
, S
) when S#state
.queued_message
=:= none
->
149 %% There is nothing to do with the hard_timer, just ignore this
150 {noreply
, S#state
{ hard_timer
= none
}};
151 handle_info(hard_timeout
, S
) ->
152 NS
= contact_tracker(S#state
.queued_message
, S
),
153 {noreply
, NS#state
{ queued_message
= none
}};
154 handle_info(soft_timeout
, S
) ->
156 NS
= contact_tracker(S
),
158 handle_info(_Info
, State
) ->
161 %%--------------------------------------------------------------------
162 %% Function: terminate(Reason, State) -> void()
163 %% Description: This function is called by a gen_server when it is about to
164 %% terminate. It should be the opposite of Module:init/1 and do any necessary
165 %% cleaning up. When it returns, the gen_server terminates with Reason.
166 %% The return value is ignored.
167 %%--------------------------------------------------------------------
168 %% XXX: Cancel timers for completeness.
169 terminate(_Reason
, S
) ->
170 _NS
= contact_tracker(stopped
, S
),
173 %%--------------------------------------------------------------------
174 %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
175 %% Description: Convert process state when code is changed
176 %%--------------------------------------------------------------------
177 code_change(_OldVsn
, State
, _Extra
) ->
180 %%--------------------------------------------------------------------
181 %%% Internal functions
182 %%--------------------------------------------------------------------
184 contact_tracker(S
) ->
185 contact_tracker(none
, S
).
187 contact_tracker(Event
, S
) ->
188 NewUrl
= build_tracker_url(S
, Event
),
189 error_logger:info_report([{contacting_tracker
, NewUrl
}]),
190 case http_gzip:request(NewUrl
) of
191 {ok
, {{_
, 200, _
}, _
, Body
}} ->
192 handle_tracker_response(etorrent_bcoding:decode(Body
), S
);
193 {error
, etimedout
} ->
195 {error
,econnrefused
} ->
197 {error
, session_remotly_closed
} ->
202 handle_tracker_response(BC
, S
) ->
203 handle_tracker_response(BC
,
204 fetch_error_message(BC
),
205 fetch_warning_message(BC
),
208 handle_tracker_response(BC
, {string
, E
}, _WM
, S
) ->
209 etorrent_t_control:tracker_error_report(S#state
.control_pid
, E
),
210 handle_timeout(BC
, S
);
211 handle_tracker_response(BC
, none
, {string
, W
}, S
) ->
212 etorrent_t_control:tracker_warning_report(S#state
.control_pid
, W
),
213 handle_tracker_response(BC
, none
, none
, S
);
214 handle_tracker_response(BC
, none
, none
, S
) ->
216 etorrent_choker:add_peers(S#state
.torrent_id
,
218 %% Update the state of the torrent
219 ok
= etorrent_torrent:statechange(S#state
.torrent_id
,
221 decode_integer("complete", BC
),
222 decode_integer("incomplete", BC
)}),
224 TrackerId
= tracker_id(BC
),
225 handle_timeout(BC
, S#state
{ trackerid
= TrackerId
}).
228 Interval
= timer:seconds(?DEFAULT_CONNECTION_TIMEOUT_INTERVAL
),
229 MinInterval
= timer:seconds(?DEFAULT_CONNECTION_TIMEOUT_MIN_INTERVAL
),
230 handle_timeout(Interval
, MinInterval
, S
).
232 handle_timeout(BC
, S
) ->
233 Interval
= response_interval(BC
),
234 MinInterval
= response_mininterval(BC
),
235 handle_timeout(Interval
, MinInterval
, S
).
237 handle_timeout(Interval
, MinInterval
, S
) ->
238 NS
= cancel_timers(S
),
239 NNS
= case MinInterval
of
242 I
when is_integer(I
) ->
243 {ok
, TRef
} = timer:send_after(timer:seconds(I
), hard_timeout
),
244 NS#state
{ hard_timer
= TRef
}
246 {ok
, TRef2
} = timer:send_after(timer:seconds(Interval
), soft_timeout
),
247 NNS#state
{ soft_timer
= TRef2
}.
250 NS
= case S#state
.hard_timer
of
255 S#state
{ hard_timer
= none
}
257 case NS#state
.soft_timer
of
262 NS#state
{ soft_timer
= none
}
266 construct_headers([], HeaderLines
) ->
267 lists:concat(lists:reverse(HeaderLines
));
268 construct_headers([{Key
, Value
}], HeaderLines
) ->
269 Data
= lists:concat([Key
, "=", Value
]),
270 construct_headers([], [Data
| HeaderLines
]);
271 construct_headers([{Key
, Value
} | Rest
], HeaderLines
) ->
272 Data
= lists:concat([Key
, "=", Value
, "&"]),
273 construct_headers(Rest
, [Data
| HeaderLines
]).
275 build_tracker_url(S
, Event
) ->
276 [R
] = etorrent_torrent:select(S#state
.torrent_id
),
277 {ok
, Port
} = application:get_env(etorrent
, port
),
278 Request
= [{"info_hash",
279 etorrent_utils:build_encoded_form_rfc1738(S#state
.info_hash
)},
281 etorrent_utils:build_encoded_form_rfc1738(S#state
.peer_id
)},
282 {"uploaded", R#torrent
.uploaded
},
283 {"downloaded", R#torrent
.downloaded
},
284 {"left", R#torrent
.left
},
289 started
-> [{"event", "started"} | Request
];
290 stopped
-> [{"event", "stopped"} | Request
];
291 completed
-> [{"event", "completed"} | Request
]
293 lists:concat([S#state
.url
, "?", construct_headers(EReq
, [])]).
295 %%% Tracker response lookup functions
296 response_interval(BC
) ->
297 {integer, R
} = etorrent_bcoding:search_dict_default({string
, "interval"},
300 ?DEFAULT_REQUEST_TIMEOUT
}),
303 response_mininterval(BC
) ->
304 X
= etorrent_bcoding:search_dict_default({string
, "min interval"},
312 %%--------------------------------------------------------------------
313 %% Function: decode_ips(IpData) -> [{IP, Port}]
314 %% Description: Decode the IP response from the tracker
315 %%--------------------------------------------------------------------
319 decode_ips([], Accum
) ->
321 decode_ips([IPDict
| Rest
], Accum
) ->
322 {string
, IP
} = etorrent_bcoding:search_dict({string
, "ip"}, IPDict
),
323 {integer, Port
} = etorrent_bcoding:search_dict({string
, "port"},
325 decode_ips(Rest
, [{IP
, Port
} | Accum
]);
326 decode_ips(<<>>, Accum
) ->
328 decode_ips(<<B1:8, B2:8, B3:8, B4:8, Port:16/big
, Rest
/binary>>, Accum
) ->
329 decode_ips(Rest
, [{{B1
, B2
, B3
, B4
}, Port
} | Accum
]).
332 case etorrent_bcoding:search_dict_default({string
, "peers"}, BC
, none
) of
336 decode_ips(list_to_binary(Ips
));
342 etorrent_bcoding:search_dict_default({string
, "trackerid"},
344 tracker_id_not_given
).
346 decode_integer(Target
, BC
) ->
347 case etorrent_bcoding:search_dict_default({string
, Target
}, BC
, none
) of
354 fetch_error_message(BC
) ->
355 etorrent_bcoding:search_dict_default({string
, "failure reason"}, BC
, none
).
357 fetch_warning_message(BC
) ->
358 etorrent_bcoding:search_dict_default({string
, "warning message"}, BC
, none
).