transmission/libtransmission/announcer.cc

1803 lines
52 KiB
C++

// This file Copyright © Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <algorithm>
#include <array>
#include <chrono> // operator""ms
#include <cstddef> // size_t
#include <cstdint>
#include <ctime>
#include <deque>
#include <iterator>
#include <map>
#include <memory>
#include <numeric>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <fmt/core.h>
#define LIBTRANSMISSION_ANNOUNCER_MODULE
#include "libtransmission/transmission.h"
#include "libtransmission/announce-list.h"
#include "libtransmission/announcer-common.h"
#include "libtransmission/announcer.h"
#include "libtransmission/crypto-utils.h" /* tr_rand_int() */
#include "libtransmission/interned-string.h" // tr_interned_string
#include "libtransmission/log.h"
#include "libtransmission/session.h"
#include "libtransmission/timer.h"
#include "libtransmission/torrent.h"
#include "libtransmission/tr-assert.h"
#include "libtransmission/tr-macros.h" // tr_sha1_digest_t, TR_C...
#include "libtransmission/utils.h"
#include "libtransmission/web-utils.h"
using namespace std::literals;
#define tr_logAddErrorTier(tier, msg) tr_logAddError(msg, (tier)->buildLogName())
#define tr_logAddWarnTier(tier, msg) tr_logAddWarn(msg, (tier)->buildLogName())
#define tr_logAddDebugTier(tier, msg) tr_logAddDebug(msg, (tier)->buildLogName())
#define tr_logAddTraceTier(tier, msg) tr_logAddTrace(msg, (tier)->buildLogName())
namespace
{
/* unless the tracker says otherwise, rescrape this frequently */
auto constexpr DefaultScrapeIntervalSec = 60 * 30;
/* the value of the 'numwant' argument passed in tracker requests. */
auto constexpr Numwant = 80;
/* how often to announce & scrape */
auto constexpr MaxAnnouncesPerUpkeep = 20;
auto constexpr MaxScrapesPerUpkeep = 20;
/* how many infohashes to remove when we get a scrape-too-long error */
auto constexpr TrMultiscrapeStep = 5;
struct StopsCompare
{
[[nodiscard]] static constexpr auto compare(tr_announce_request const& one, tr_announce_request const& two) noexcept // <=>
{
// primary key: volume of data transferred
auto const ax = one.up + one.down;
auto const bx = two.up + two.down;
if (auto const val = tr_compare_3way(ax, bx); val != 0)
{
return val;
}
// secondary key: the torrent's info_hash
for (size_t i = 0, n = sizeof(tr_sha1_digest_t); i < n; ++i)
{
if (auto const val = tr_compare_3way(one.info_hash[i], two.info_hash[i]); val != 0)
{
return val;
}
}
// tertiary key: the tracker's announce url
return tr_compare_3way(one.announce_url, two.announce_url);
}
[[nodiscard]] constexpr auto operator()(tr_announce_request const& one, tr_announce_request const& two) const noexcept
{
return compare(one, two) < 0;
}
};
} // namespace
// ---
struct tr_scrape_info
{
int multiscrape_max;
tr_interned_string scrape_url;
constexpr tr_scrape_info(tr_interned_string scrape_url_in, int const multiscrape_max_in)
: multiscrape_max{ multiscrape_max_in }
, scrape_url{ scrape_url_in }
{
}
};
/**
* "global" (per-tr_session) fields
*/
class tr_announcer_impl final : public tr_announcer
{
public:
explicit tr_announcer_impl(tr_session* session_in, tr_announcer_udp& announcer_udp)
: session{ session_in }
, announcer_udp_{ announcer_udp }
, upkeep_timer_{ session_in->timerMaker().create() }
{
upkeep_timer_->set_callback([this]() { this->upkeep(); });
upkeep_timer_->start_repeating(UpkeepInterval);
}
~tr_announcer_impl() override
{
flushCloseMessages();
}
tr_announcer_impl(tr_announcer_impl&&) = delete;
tr_announcer_impl(tr_announcer_impl const&) = delete;
tr_announcer_impl& operator=(tr_announcer_impl&&) = delete;
tr_announcer_impl& operator=(tr_announcer_impl const&) = delete;
tr_torrent_announcer* addTorrent(tr_torrent* tor, tr_tracker_callback callback) override;
void startTorrent(tr_torrent* tor) override;
void stopTorrent(tr_torrent* tor) override;
void resetTorrent(tr_torrent* tor) override;
void removeTorrent(tr_torrent* tor) override;
void startShutdown() override
{
is_shutting_down_ = true;
flushCloseMessages();
}
void upkeep() override;
void onAnnounceDone(int tier_id, tr_announce_event event, bool is_running_on_success, tr_announce_response const& response);
void onScrapeDone(tr_scrape_response const& response);
[[nodiscard]] tr_scrape_info* scrape_info(tr_interned_string url)
{
if (std::empty(url))
{
return nullptr;
}
auto const [it, is_new] = scrape_info_.try_emplace(url, url, TR_MULTISCRAPE_MAX);
return &it->second;
}
void scrape(tr_scrape_request const& request, tr_scrape_response_func on_response)
{
TR_ASSERT(!is_shutting_down_);
if (auto const scrape_sv = request.scrape_url.sv();
tr_strv_starts_with(scrape_sv, "http://"sv) || tr_strv_starts_with(scrape_sv, "https://"sv))
{
tr_tracker_http_scrape(session, request, std::move(on_response));
}
else if (tr_strv_starts_with(scrape_sv, "udp://"sv))
{
announcer_udp_.scrape(request, std::move(on_response));
}
else
{
tr_logAddError(fmt::format(_("Unsupported URL: '{url}'"), fmt::arg("url", scrape_sv)));
}
}
void announce(tr_announce_request const& request, tr_announce_response_func on_response)
{
TR_ASSERT(!is_shutting_down_ || request.event == TR_ANNOUNCE_EVENT_STOPPED);
if (auto const announce_sv = request.announce_url.sv();
tr_strv_starts_with(announce_sv, "http://"sv) || tr_strv_starts_with(announce_sv, "https://"sv))
{
tr_tracker_http_announce(session, request, std::move(on_response));
}
else if (tr_strv_starts_with(announce_sv, "udp://"sv))
{
announcer_udp_.announce(request, std::move(on_response));
}
else
{
tr_logAddWarn(fmt::format(_("Unsupported URL: '{url}'"), fmt::arg("url", announce_sv)));
}
}
tr_session* const session;
private:
void flushCloseMessages()
{
for (auto& stop : stops_)
{
announce(stop, [](tr_announce_response const& /*response*/) {});
}
stops_.clear();
}
static auto constexpr UpkeepInterval = 500ms;
tr_announcer_udp& announcer_udp_;
std::map<tr_interned_string, tr_scrape_info> scrape_info_;
std::unique_ptr<libtransmission::Timer> const upkeep_timer_;
std::set<tr_announce_request, StopsCompare> stops_;
bool is_shutting_down_ = false;
};
std::unique_ptr<tr_announcer> tr_announcer::create(tr_session* session, tr_announcer_udp& announcer_udp)
{
TR_ASSERT(session != nullptr);
return std::make_unique<tr_announcer_impl>(session, announcer_udp);
}
// ---
/* a row in tr_tier's list of trackers */
struct tr_tracker
{
explicit tr_tracker(tr_announcer_impl* announcer, tr_announce_list::tracker_info const& info)
: announce_url{ info.announce }
, announce_parsed{ info.announce_parsed }
, scrape_info{ std::empty(info.scrape) ? nullptr : announcer->scrape_info(info.scrape) }
, id{ info.id }
{
}
[[nodiscard]] time_t getRetryInterval() const
{
switch (consecutive_failures)
{
case 0:
return 0U;
case 1:
return 20U;
case 2:
return tr_rand_int(60U) + 60U * 5U;
case 3:
return tr_rand_int(60U) + 60U * 15U;
case 4:
return tr_rand_int(60U) + 60U * 30U;
case 5:
return tr_rand_int(60U) + 60U * 60U;
default:
return tr_rand_int(60U) + 60U * 120U;
}
}
[[nodiscard]] constexpr auto seeder_count() const noexcept
{
return seeder_count_;
}
constexpr bool set_seeder_count(std::optional<int64_t> seeder_count_in) noexcept
{
if (seeder_count_in >= 0)
{
seeder_count_ = seeder_count_in;
return true;
}
return false;
}
[[nodiscard]] constexpr auto leecher_count() const noexcept
{
return leecher_count_;
}
constexpr bool set_leecher_count(std::optional<int64_t> leecher_count_in) noexcept
{
if (leecher_count_in >= 0)
{
leecher_count_ = leecher_count_in;
return true;
}
return false;
}
[[nodiscard]] constexpr auto download_count() const noexcept
{
return download_count_;
}
constexpr bool set_download_count(std::optional<int64_t> download_count_in) noexcept
{
if (download_count_in >= 0)
{
download_count_ = download_count_in;
return true;
}
return false;
}
[[nodiscard]] constexpr auto downloader_count() const noexcept
{
return downloader_count_;
}
constexpr bool set_downloader_count(std::optional<int64_t> downloader_count_in) noexcept
{
if (downloader_count_in >= 0)
{
downloader_count_ = downloader_count_in;
return true;
}
return false;
}
tr_interned_string const announce_url;
tr_url_parsed_t const announce_parsed;
tr_scrape_info* const scrape_info;
std::string tracker_id;
int consecutive_failures = 0;
tr_tracker_id_t const id;
private:
std::optional<int64_t> seeder_count_;
std::optional<int64_t> leecher_count_;
std::optional<int64_t> download_count_;
std::optional<int64_t> downloader_count_;
};
// ---
/** @brief A group of trackers in a single tier, as per the multitracker spec */
struct tr_tier
{
tr_tier(tr_announcer_impl* announcer, tr_torrent* tor_in, std::vector<tr_announce_list::tracker_info const*> const& infos)
: tor{ tor_in }
{
trackers.reserve(std::size(infos));
for (auto const* info : infos)
{
trackers.emplace_back(announcer, *info);
}
useNextTracker();
scrapeSoon();
}
[[nodiscard]] tr_tracker* currentTracker()
{
if (!current_tracker_index_)
{
return nullptr;
}
TR_ASSERT(*current_tracker_index_ < std::size(trackers));
return &trackers[*current_tracker_index_];
}
[[nodiscard]] tr_tracker const* currentTracker() const
{
if (!current_tracker_index_)
{
return nullptr;
}
TR_ASSERT(*current_tracker_index_ < std::size(trackers));
return &trackers[*current_tracker_index_];
}
[[nodiscard]] constexpr bool needsToAnnounce(time_t now) const
{
return !isAnnouncing && !isScraping && announceAt != 0 && announceAt <= now && !std::empty(announce_events);
}
[[nodiscard]] bool needsToScrape(time_t now) const
{
auto const* const tracker = currentTracker();
return !isScraping && scrapeAt != 0 && scrapeAt <= now && tracker != nullptr && tracker->scrape_info != nullptr;
}
[[nodiscard]] auto countDownloaders() const
{
auto const* const tracker = currentTracker();
return tracker == nullptr ? 0 : tracker->downloader_count().value_or(-1) + tracker->leecher_count().value_or(-1);
}
tr_tracker* useNextTracker()
{
// move our index to the next tracker in the tier
if (std::empty(trackers))
{
current_tracker_index_ = std::nullopt;
}
else if (!current_tracker_index_)
{
current_tracker_index_ = 0;
}
else
{
current_tracker_index_ = (*current_tracker_index_ + 1) % std::size(trackers);
}
// reset some of the tier's fields
scrapeIntervalSec = DefaultScrapeIntervalSec;
announceIntervalSec = DefaultAnnounceIntervalSec;
announceMinIntervalSec = DefaultAnnounceMinIntervalSec;
isAnnouncing = false;
isScraping = false;
lastAnnounceStartTime = 0;
lastScrapeStartTime = 0;
return currentTracker();
}
[[nodiscard]] std::optional<size_t> indexOf(tr_interned_string announce_url) const
{
for (size_t i = 0, n = std::size(trackers); i < n; ++i)
{
if (announce_url == trackers[i].announce_url)
{
return i;
}
}
return std::nullopt;
}
[[nodiscard]] auto buildLogName() const
{
auto const* const tracker = currentTracker();
return tracker != nullptr ?
fmt::format("{:s} at {:s}:{:d}", tor->name(), tracker->announce_parsed.host, tracker->announce_parsed.port) :
fmt::format("{:s} at ?", tor->name());
}
[[nodiscard]] bool canManualAnnounce() const
{
return this->manualAnnounceAllowedAt <= tr_time();
}
void scheduleNextScrape()
{
scheduleNextScrape(this->scrapeIntervalSec);
}
void scrapeSoon()
{
scheduleNextScrape(0);
}
void scheduleNextScrape(time_t interval_secs)
{
this->scrapeAt = getNextScrapeTime(tor->session, this, interval_secs);
}
std::deque<tr_announce_event> announce_events;
std::string last_announce_str;
std::string last_scrape_str;
/* number of up/down/corrupt bytes since the last time we sent an
* "event=stopped" message that was acknowledged by the tracker */
std::array<uint64_t, 3> byteCounts = {};
std::vector<tr_tracker> trackers;
std::optional<size_t> current_tracker_index_;
tr_torrent* const tor;
time_t scrapeAt = 0;
time_t lastScrapeStartTime = 0;
time_t lastScrapeTime = 0;
time_t announceAt = 0;
time_t manualAnnounceAllowedAt = 0;
time_t lastAnnounceStartTime = 0;
time_t lastAnnounceTime = 0;
int const id = next_key++;
int announce_event_priority = 0;
int scrapeIntervalSec = DefaultScrapeIntervalSec;
int announceIntervalSec = DefaultAnnounceIntervalSec;
int announceMinIntervalSec = DefaultAnnounceMinIntervalSec;
size_t lastAnnouncePeerCount = 0;
bool lastScrapeSucceeded = false;
bool lastScrapeTimedOut = false;
bool lastAnnounceSucceeded = false;
bool lastAnnounceTimedOut = false;
bool isRunning = false;
bool isAnnouncing = false;
bool isScraping = false;
private:
// unless the tracker says otherwise, this is the announce interval
static auto constexpr DefaultAnnounceIntervalSec = 60 * 10;
// unless the tracker says otherwise, this is the announce min_interval
static auto constexpr DefaultAnnounceMinIntervalSec = 60 * 2;
[[nodiscard]] static time_t getNextScrapeTime(tr_session const* session, tr_tier const* tier, time_t interval_secs)
{
// Maybe don't scrape paused torrents
if (!tier->isRunning && !session->shouldScrapePausedTorrents())
{
return 0;
}
/* Add the interval, and then increment to the nearest 10th second.
* The latter step is to increase the odds of several torrents coming
* due at the same time to improve multiscrape. */
auto ret = tr_time() + interval_secs;
while (ret % 10U != 0U)
{
++ret;
}
return ret;
}
static inline int next_key = 0;
};
// ---
/**
* @brief Opaque, per-torrent data structure for tracker announce information
*
* this opaque data structure can be found in tr_torrent.tiers
*/
struct tr_torrent_announcer
{
tr_torrent_announcer(tr_announcer_impl* announcer, tr_torrent* tor)
{
// build the trackers
auto tier_to_infos = std::map<tr_tracker_tier_t, std::vector<tr_announce_list::tracker_info const*>>{};
auto const announce_list = getAnnounceList(tor);
for (auto const& info : announce_list)
{
tier_to_infos[info.tier].emplace_back(&info);
}
for (auto const& [tier_num, infos] : tier_to_infos)
{
tiers.emplace_back(announcer, tor, infos);
}
}
tr_tier* getTier(int tier_id)
{
for (auto& tier : tiers)
{
if (tier.id == tier_id)
{
return &tier;
}
}
return nullptr;
}
tr_tier* getTierFromScrape(tr_interned_string scrape_url)
{
for (auto& tier : tiers)
{
auto const* const tracker = tier.currentTracker();
if (tracker != nullptr && tracker->scrape_info != nullptr && tracker->scrape_info->scrape_url == scrape_url)
{
return &tier;
}
}
return nullptr;
}
[[nodiscard]] bool canManualAnnounce() const
{
return std::any_of(std::begin(tiers), std::end(tiers), [](auto const& tier) { return tier.canManualAnnounce(); });
}
[[nodiscard]] bool findTracker(
tr_interned_string announce_url,
tr_tier const** setme_tier,
tr_tracker const** setme_tracker) const
{
for (auto const& tier : tiers)
{
for (auto const& tracker : tier.trackers)
{
if (tracker.announce_url == announce_url)
{
*setme_tier = &tier;
*setme_tracker = &tracker;
return true;
}
}
}
return false;
}
std::vector<tr_tier> tiers;
tr_tracker_callback callback;
private:
[[nodiscard]] static tr_announce_list getAnnounceList(tr_torrent const* tor)
{
auto announce_list = tor->announce_list();
// if it's a public torrent, add the default trackers
if (tor->is_public())
{
announce_list.add(tor->session->defaultTrackers());
}
return announce_list;
}
};
// --- PUBLISH
namespace
{
namespace publish_helpers
{
void publishMessage(tr_tier* tier, std::string_view msg, tr_tracker_event::Type type)
{
if (tier != nullptr && tier->tor != nullptr && tier->tor->torrent_announcer != nullptr &&
tier->tor->torrent_announcer->callback != nullptr)
{
auto* const ta = tier->tor->torrent_announcer;
auto event = tr_tracker_event{};
event.type = type;
event.text = msg;
if (auto const* const current_tracker = tier->currentTracker(); current_tracker != nullptr)
{
event.announce_url = current_tracker->announce_url;
}
ta->callback(*tier->tor, &event);
}
}
void publishErrorClear(tr_tier* tier)
{
publishMessage(tier, ""sv, tr_tracker_event::Type::ErrorClear);
}
void publishWarning(tr_tier* tier, std::string_view msg)
{
publishMessage(tier, msg, tr_tracker_event::Type::Warning);
}
void publishError(tr_tier* tier, std::string_view msg)
{
publishMessage(tier, msg, tr_tracker_event::Type::Error);
}
void publishPeerCounts(tr_tier* tier, std::optional<int64_t> seeders, std::optional<int64_t> leechers)
{
if (tier->tor->torrent_announcer->callback != nullptr)
{
auto e = tr_tracker_event{};
e.type = tr_tracker_event::Type::Counts;
e.seeders = seeders;
e.leechers = leechers;
tr_logAddDebugTier(
tier,
fmt::format("peer counts: {} seeders, {} leechers.", seeders.value_or(-1), leechers.value_or(-1)));
tier->tor->torrent_announcer->callback(*tier->tor, &e);
}
}
void publishPeersPex(tr_tier* tier, std::vector<tr_pex> const& pex)
{
if (tier->tor->torrent_announcer->callback != nullptr)
{
auto e = tr_tracker_event{};
e.type = tr_tracker_event::Type::Peers;
e.pex = pex;
tr_logAddDebugTier(tier, fmt::format("tracker gave a list of {} peers.", std::size(pex)));
tier->tor->torrent_announcer->callback(*tier->tor, &e);
}
}
} // namespace publish_helpers
} // namespace
// ---
tr_torrent_announcer* tr_announcer_impl::addTorrent(tr_torrent* tor, tr_tracker_callback callback)
{
TR_ASSERT(tr_isTorrent(tor));
auto* ta = new tr_torrent_announcer{ this, tor };
ta->callback = std::move(callback);
return ta;
}
// ---
bool tr_announcerCanManualAnnounce(tr_torrent const* tor)
{
TR_ASSERT(tr_isTorrent(tor));
TR_ASSERT(tor->torrent_announcer != nullptr);
return tor->is_running() && tor->torrent_announcer->canManualAnnounce();
}
time_t tr_announcerNextManualAnnounce(tr_torrent const* tor)
{
time_t ret = ~(time_t)0;
for (auto const& tier : tor->torrent_announcer->tiers)
{
if (tier.isRunning)
{
ret = std::min(ret, tier.manualAnnounceAllowedAt);
}
}
return ret;
}
namespace
{
namespace announce_helpers
{
void tr_logAddTrace_tier_announce_queue(tr_tier const* tier)
{
if (!tr_logLevelIsActive(TR_LOG_TRACE) || std::empty(tier->announce_events))
{
return;
}
auto buf = std::string{};
auto const& events = tier->announce_events;
buf.reserve(std::size(events) * 20);
for (size_t i = 0, n = std::size(events); i < n; ++i)
{
fmt::format_to(std::back_inserter(buf), "[{:d}:{:s}]", i, tr_announce_event_get_string(events[i]));
}
tr_logAddTraceTier(tier, std::move(buf));
}
// higher priorities go to the front of the announce queue
void tier_update_announce_priority(tr_tier* tier)
{
int priority = -1;
for (auto const& event : tier->announce_events)
{
priority = std::max(priority, int{ event });
}
tier->announce_event_priority = priority;
}
void tier_announce_remove_trailing(tr_tier* tier, tr_announce_event e)
{
while (!std::empty(tier->announce_events) && tier->announce_events.back() == e)
{
tier->announce_events.resize(std::size(tier->announce_events) - 1);
}
tier_update_announce_priority(tier);
}
void tier_announce_event_push(tr_tier* tier, tr_announce_event e, time_t announce_at)
{
TR_ASSERT(tier != nullptr);
tr_logAddTrace_tier_announce_queue(tier);
tr_logAddTraceTier(tier, fmt::format("queued '{}'", tr_announce_event_get_string(e)));
auto& events = tier->announce_events;
if (!std::empty(events))
{
/* special case #1: if we're adding a "stopped" event,
* dump everything leading up to it except "completed" */
if (e == TR_ANNOUNCE_EVENT_STOPPED)
{
bool const has_completed = std::count(std::begin(events), std::end(events), TR_ANNOUNCE_EVENT_COMPLETED) != 0;
events.clear();
if (has_completed)
{
events.push_back(TR_ANNOUNCE_EVENT_COMPLETED);
}
}
/* special case #2: dump all empty strings leading up to this event */
tier_announce_remove_trailing(tier, TR_ANNOUNCE_EVENT_NONE);
/* special case #3: no consecutive duplicates */
tier_announce_remove_trailing(tier, e);
}
/* add it */
events.push_back(e);
tier->announceAt = announce_at;
tier_update_announce_priority(tier);
tr_logAddTrace_tier_announce_queue(tier);
tr_logAddTraceTier(tier, fmt::format("announcing in {} seconds", difftime(announce_at, tr_time())));
}
auto tier_announce_event_pull(tr_tier* tier)
{
auto const e = tier->announce_events.front();
tier->announce_events.pop_front();
tier_update_announce_priority(tier);
return e;
}
bool isUnregistered(char const* errmsg)
{
auto const lower = tr_strlower(errmsg != nullptr ? errmsg : "");
auto constexpr Keys = std::array<std::string_view, 2>{ "unregistered torrent"sv, "torrent not registered"sv };
return std::any_of(std::begin(Keys), std::end(Keys), [&lower](auto const& key) { return tr_strv_contains(lower, key); });
}
void on_announce_error(tr_tier* tier, char const* err, tr_announce_event e)
{
using namespace announce_helpers;
auto* current_tracker = tier->currentTracker();
std::string const announce_url = current_tracker != nullptr ? tr_urlTrackerLogName(current_tracker->announce_url) :
"nullptr";
/* increment the error count */
if (current_tracker != nullptr)
{
++current_tracker->consecutive_failures;
}
/* set the error message */
tier->last_announce_str = err;
/* switch to the next tracker */
current_tracker = tier->useNextTracker();
if (isUnregistered(err))
{
tr_logAddErrorTier(
tier,
fmt::format(_("Announce error: {error} ({url})"), fmt::arg("error", err), fmt::arg("url", announce_url)));
}
else
{
/* schedule a reannounce */
auto const interval = current_tracker->getRetryInterval();
tr_logAddWarnTier(
tier,
fmt::format(
tr_ngettext(
"Announce error: {error} (Retrying in {count} second) ({url})",
"Announce error: {error} (Retrying in {count} seconds) ({url})",
interval),
fmt::arg("error", err),
fmt::arg("count", interval),
fmt::arg("url", announce_url)));
tier_announce_event_push(tier, e, tr_time() + interval);
}
}
[[nodiscard]] tr_announce_request create_announce_request(
tr_announcer_impl const* const announcer,
tr_torrent* const tor,
tr_tier const* const tier,
tr_announce_event const event)
{
auto const* const current_tracker = tier->currentTracker();
TR_ASSERT(current_tracker != nullptr);
auto req = tr_announce_request{};
req.port = announcer->session->advertisedPeerPort();
req.announce_url = current_tracker->announce_url;
req.tracker_id = current_tracker->tracker_id;
req.info_hash = tor->info_hash();
req.peer_id = tor->peer_id();
req.up = tier->byteCounts[TR_ANN_UP];
req.down = tier->byteCounts[TR_ANN_DOWN];
req.corrupt = tier->byteCounts[TR_ANN_CORRUPT];
req.leftUntilComplete = tor->has_metainfo() ? tor->total_size() - tor->has_total() : INT64_MAX;
req.event = event;
req.numwant = event == TR_ANNOUNCE_EVENT_STOPPED ? 0 : Numwant;
req.key = tor->announce_key();
req.partial_seed = tor->is_partial_seed();
req.log_name = tier->buildLogName();
return req;
}
[[nodiscard]] tr_tier* getTier(tr_announcer_impl* announcer, tr_sha1_digest_t const& info_hash, int tier_id)
{
if (announcer == nullptr)
{
return nullptr;
}
auto* const tor = announcer->session->torrents().get(info_hash);
if (tor == nullptr || tor->torrent_announcer == nullptr)
{
return nullptr;
}
return tor->torrent_announcer->getTier(tier_id);
}
} // namespace announce_helpers
void torrentAddAnnounce(tr_torrent* tor, tr_announce_event e, time_t announce_at)
{
using namespace announce_helpers;
// tell each tier to announce
for (auto& tier : tor->torrent_announcer->tiers)
{
tier_announce_event_push(&tier, e, announce_at);
}
}
} // namespace
void tr_announcer_impl::onAnnounceDone(
int tier_id,
tr_announce_event event,
bool is_running_on_success,
tr_announce_response const& response)
{
using namespace announce_helpers;
using namespace publish_helpers;
auto* const tier = getTier(this, response.info_hash, tier_id);
if (tier == nullptr)
{
return;
}
auto const now = tr_time();
tr_logAddTraceTier(
tier,
fmt::format(
"Got announce response: "
"connected:{} "
"timeout:{} "
"seeders:{} "
"leechers:{} "
"downloads:{} "
"interval:{} "
"min_interval:{} "
"tracker_id_str:{} "
"pex:{} "
"pex6:{} "
"err:{} "
"warn:{}",
response.did_connect,
response.did_timeout,
response.seeders.value_or(-1),
response.leechers.value_or(-1),
response.downloads.value_or(-1),
response.interval,
response.min_interval,
(!std::empty(response.tracker_id) ? response.tracker_id.c_str() : "none"),
std::size(response.pex),
std::size(response.pex6),
(!std::empty(response.errmsg) ? response.errmsg.c_str() : "none"),
(!std::empty(response.warning) ? response.warning.c_str() : "none")));
tier->lastAnnounceTime = now;
tier->lastAnnounceTimedOut = response.did_timeout;
tier->lastAnnounceSucceeded = false;
tier->isAnnouncing = false;
tier->manualAnnounceAllowedAt = now + tier->announceMinIntervalSec;
if (response.external_ip)
{
session->set_global_address(*response.external_ip);
}
if (!response.did_connect)
{
on_announce_error(tier, _("Could not connect to tracker"), event);
}
else if (response.did_timeout)
{
on_announce_error(tier, _("Tracker did not respond"), event);
}
else if (!std::empty(response.errmsg))
{
/* If the torrent's only tracker returned an error, publish it.
Don't bother publishing if there are other trackers -- it's
all too common for people to load up dozens of dead trackers
in a torrent's metainfo... */
if (std::size(tier->tor->announce_list()) < 2U)
{
publishError(tier, response.errmsg);
}
on_announce_error(tier, response.errmsg.c_str(), event);
}
else
{
auto const is_stopped = event == TR_ANNOUNCE_EVENT_STOPPED;
auto scrape_fields = uint8_t{};
publishErrorClear(tier);
auto* const tracker = tier->currentTracker();
if (tracker != nullptr)
{
tracker->consecutive_failures = 0;
if (tracker->set_seeder_count(response.seeders))
{
++scrape_fields;
}
if (tracker->set_leecher_count(response.leechers))
{
++scrape_fields;
}
if (tracker->set_download_count(response.downloads))
{
++scrape_fields;
}
if (!std::empty(response.tracker_id))
{
tracker->tracker_id = response.tracker_id;
}
}
if (auto const& warning = response.warning; !std::empty(warning))
{
tier->last_announce_str = warning;
tr_logAddTraceTier(tier, fmt::format("tracker gave '{}'", warning));
publishWarning(tier, warning);
}
else
{
tier->last_announce_str = _("Success");
}
if (response.min_interval != 0)
{
tier->announceMinIntervalSec = response.min_interval;
}
if (response.interval != 0)
{
tier->announceIntervalSec = response.interval;
}
if (!std::empty(response.pex))
{
publishPeersPex(tier, response.pex);
}
if (!std::empty(response.pex6))
{
publishPeersPex(tier, response.pex6);
}
publishPeerCounts(tier, response.seeders, response.leechers);
tier->isRunning = is_running_on_success;
/* if the tracker included scrape fields in its announce response,
then a separate scrape isn't needed */
if (scrape_fields >= 3U || (scrape_fields >= 1U && tracker->scrape_info == nullptr))
{
tr_logAddTraceTier(
tier,
fmt::format(
"Announce response has scrape info; bumping next scrape to {} seconds from now.",
tier->scrapeIntervalSec));
tier->scheduleNextScrape();
tier->lastScrapeTime = now;
tier->lastScrapeSucceeded = true;
}
else if (tier->lastScrapeTime + tier->scrapeIntervalSec <= now)
{
tier->scrapeSoon();
}
tier->lastAnnounceSucceeded = true;
tier->lastAnnouncePeerCount = std::size(response.pex) + std::size(response.pex6);
if (is_stopped)
{
/* now that we've successfully stopped the torrent,
* we can reset the up/down/corrupt count we've kept
* for this tracker */
tier->byteCounts[TR_ANN_UP] = 0;
tier->byteCounts[TR_ANN_DOWN] = 0;
tier->byteCounts[TR_ANN_CORRUPT] = 0;
}
if (!is_stopped && std::empty(tier->announce_events))
{
/* the queue is empty, so enqueue a periodic update */
int const i = tier->announceIntervalSec;
tr_logAddTraceTier(tier, fmt::format("Sending periodic reannounce in {} seconds", i));
tier_announce_event_push(tier, TR_ANNOUNCE_EVENT_NONE, now + i);
}
}
}
void tr_announcer_impl::startTorrent(tr_torrent* tor)
{
torrentAddAnnounce(tor, TR_ANNOUNCE_EVENT_STARTED, tr_time());
}
void tr_announcerManualAnnounce(tr_torrent* tor)
{
torrentAddAnnounce(tor, TR_ANNOUNCE_EVENT_NONE, tr_time());
}
void tr_announcer_impl::stopTorrent(tr_torrent* tor)
{
torrentAddAnnounce(tor, TR_ANNOUNCE_EVENT_STOPPED, tr_time());
}
void tr_announcerTorrentCompleted(tr_torrent* tor)
{
torrentAddAnnounce(tor, TR_ANNOUNCE_EVENT_COMPLETED, tr_time());
}
void tr_announcerChangeMyPort(tr_torrent* tor)
{
torrentAddAnnounce(tor, TR_ANNOUNCE_EVENT_STARTED, tr_time());
}
// ---
void tr_announcerAddBytes(tr_torrent* tor, int type, uint32_t n_bytes)
{
TR_ASSERT(tr_isTorrent(tor));
TR_ASSERT(type == TR_ANN_UP || type == TR_ANN_DOWN || type == TR_ANN_CORRUPT);
for (auto& tier : tor->torrent_announcer->tiers)
{
tier.byteCounts[type] += n_bytes;
}
}
// ---
void tr_announcer_impl::removeTorrent(tr_torrent* tor)
{
using namespace announce_helpers;
// FIXME(ckerr)
auto* const ta = tor->torrent_announcer;
if (ta == nullptr)
{
return;
}
for (auto const& tier : ta->tiers)
{
if (tier.isRunning && tier.lastAnnounceSucceeded)
{
stops_.emplace(create_announce_request(this, tor, &tier, TR_ANNOUNCE_EVENT_STOPPED));
}
}
tor->torrent_announcer = nullptr;
delete ta;
}
// --- SCRAPE
namespace
{
namespace on_scrape_done_helpers
{
[[nodiscard]] TR_CONSTEXPR20 bool multiscrape_too_big(std::string_view errmsg)
{
/* Found a tracker that returns some bespoke string for this case?
Add your patch here and open a PR */
auto too_long_errors = std::array<std::string_view, 3>{
"Bad Request",
"GET string too long",
"Request-URI Too Long",
};
return std::any_of(
std::begin(too_long_errors),
std::end(too_long_errors),
[&errmsg](auto const& substr) { return tr_strv_contains(errmsg, substr); });
}
void on_scrape_error(tr_session const* /*session*/, tr_tier* tier, char const* errmsg)
{
if (auto* const current_tracker = tier->currentTracker(); current_tracker != nullptr)
{
++current_tracker->consecutive_failures;
tr_logAddDebugTier(
tier,
fmt::format(
"Tracker '{}' scrape error: {} (Can retry in {} seconds)",
current_tracker->announce_parsed.authority,
errmsg,
current_tracker->getRetryInterval()));
}
// set the error message
tier->last_scrape_str = errmsg != nullptr ? errmsg : "";
tier->lastScrapeSucceeded = false;
// switch to the next tracker
if (auto* const current_tracker = tier->useNextTracker(); current_tracker != nullptr)
{
// schedule a rescrape
tier->scheduleNextScrape(current_tracker->getRetryInterval());
}
}
void checkMultiscrapeMax(tr_announcer_impl* announcer, tr_scrape_response const& response)
{
if (!multiscrape_too_big(response.errmsg))
{
return;
}
auto const& url = response.scrape_url;
auto* const scrape_info = announcer->scrape_info(url);
if (scrape_info == nullptr)
{
return;
}
// Lower the max only if it hasn't already lowered for a similar
// error. So if N parallel multiscrapes all have the same `max`
// and error out, lower the value once for that batch, not N times.
int& multiscrape_max = scrape_info->multiscrape_max;
if (multiscrape_max < response.row_count)
{
return;
}
int const n = std::max(1, int{ multiscrape_max - TrMultiscrapeStep });
if (multiscrape_max != n)
{
// don't log the full URL, since that might have a personal announce id
tr_logAddDebug(fmt::format("Reducing multiscrape max to {:d}", n), tr_urlTrackerLogName(url));
multiscrape_max = n;
}
}
} // namespace on_scrape_done_helpers
} // namespace
void tr_announcer_impl::onScrapeDone(tr_scrape_response const& response)
{
using namespace on_scrape_done_helpers;
using namespace publish_helpers;
auto const now = tr_time();
for (int i = 0; i < response.row_count; ++i)
{
auto const& row = response.rows[i];
auto* const tor = session->torrents().get(row.info_hash);
if (tor == nullptr)
{
continue;
}
auto* const tier = tor->torrent_announcer->getTierFromScrape(response.scrape_url);
if (tier == nullptr)
{
continue;
}
tr_logAddTraceTier(
tier,
fmt::format(
"scraped url:{} "
" -- "
"did_connect:{} "
"did_timeout:{} "
"seeders:{} "
"leechers:{} "
"downloads:{} "
"downloaders:{} "
"min_request_interval:{} "
"err:{} ",
response.scrape_url.sv(),
response.did_connect,
response.did_timeout,
row.seeders.value_or(-1),
row.leechers.value_or(-1),
row.downloads.value_or(-1),
row.downloaders.value_or(-1),
response.min_request_interval,
std::empty(response.errmsg) ? "none"sv : response.errmsg));
tier->isScraping = false;
tier->lastScrapeTime = now;
tier->lastScrapeSucceeded = false;
tier->lastScrapeTimedOut = response.did_timeout;
if (!response.did_connect)
{
on_scrape_error(session, tier, _("Could not connect to tracker"));
}
else if (response.did_timeout)
{
on_scrape_error(session, tier, _("Tracker did not respond"));
}
else if (!std::empty(response.errmsg))
{
on_scrape_error(session, tier, response.errmsg.c_str());
}
else
{
tier->lastScrapeSucceeded = true;
tier->scrapeIntervalSec = std::max(int{ DefaultScrapeIntervalSec }, response.min_request_interval);
tier->scheduleNextScrape();
tr_logAddTraceTier(tier, fmt::format("Scrape successful. Rescraping in {} seconds.", tier->scrapeIntervalSec));
if (tr_tracker* const tracker = tier->currentTracker(); tracker != nullptr)
{
tracker->set_seeder_count(row.seeders);
tracker->set_leecher_count(row.leechers);
tracker->set_download_count(row.downloads);
tracker->set_downloader_count(row.downloaders);
tracker->consecutive_failures = 0;
}
publishPeerCounts(tier, row.seeders, row.leechers);
}
}
checkMultiscrapeMax(this, response);
}
namespace
{
void multiscrape(tr_announcer_impl* announcer, std::vector<tr_tier*> const& tiers)
{
auto const now = tr_time();
auto requests = std::array<tr_scrape_request, MaxScrapesPerUpkeep>{};
auto request_count = size_t{};
// batch as many info_hashes into a request as we can
for (auto* tier : tiers)
{
auto const* const current_tracker = tier->currentTracker();
TR_ASSERT(current_tracker != nullptr);
if (current_tracker == nullptr)
{
continue;
}
auto const* const scrape_info = current_tracker->scrape_info;
TR_ASSERT(scrape_info != nullptr);
if (scrape_info == nullptr)
{
continue;
}
bool found = false;
/* if there's a request with this scrape URL and a free slot, use it */
for (size_t j = 0; !found && j < request_count; ++j)
{
auto* const req = &requests[j];
if (req->info_hash_count >= scrape_info->multiscrape_max)
{
continue;
}
if (scrape_info->scrape_url != req->scrape_url)
{
continue;
}
req->info_hash[req->info_hash_count] = tier->tor->info_hash();
++req->info_hash_count;
tier->isScraping = true;
tier->lastScrapeStartTime = now;
found = true;
}
/* otherwise, if there's room for another request, build a new one */
if (!found && request_count < MaxScrapesPerUpkeep)
{
auto* const req = &requests[request_count];
req->scrape_url = scrape_info->scrape_url;
req->log_name = tier->buildLogName();
req->info_hash[req->info_hash_count] = tier->tor->info_hash();
++req->info_hash_count;
tier->isScraping = true;
tier->lastScrapeStartTime = now;
++request_count;
}
}
/* send the requests we just built */
for (size_t i = 0; i < request_count; ++i)
{
announcer->scrape(
requests[i],
[session = announcer->session, announcer](tr_scrape_response const& response)
{
if (session->announcer_)
{
announcer->onScrapeDone(response);
}
});
}
}
namespace upkeep_helpers
{
int compareAnnounceTiers(tr_tier const* a, tr_tier const* b)
{
/* prefer higher-priority events */
if (auto const val = tr_compare_3way(a->announce_event_priority, b->announce_event_priority); val != 0)
{
return -val;
}
/* prefer swarms where we might upload */
if (auto const val = tr_compare_3way(a->countDownloaders(), b->countDownloaders()); val != 0)
{
return -val;
}
/* prefer swarms where we might download */
if (auto const val = tr_compare_3way(a->tor->is_done(), b->tor->is_done()); val != 0)
{
return val;
}
/* prefer larger stats, to help ensure stats get recorded when stopping on shutdown */
if (auto const val = tr_compare_3way(
a->byteCounts[TR_ANN_UP] + a->byteCounts[TR_ANN_DOWN],
b->byteCounts[TR_ANN_UP] + b->byteCounts[TR_ANN_DOWN]);
val != 0)
{
return -val;
}
// announcements that have been waiting longer go first
if (auto const val = tr_compare_3way(a->announceAt, b->announceAt); val != 0)
{
return val;
}
// the tiers are effectively equal priority, but add an arbitrary
// differentiation because ptrArray sorted mode hates equal items.
return tr_compare_3way(a, b);
}
void tierAnnounce(tr_announcer_impl* announcer, tr_tier* tier)
{
using namespace announce_helpers;
TR_ASSERT(!tier->isAnnouncing);
TR_ASSERT(!std::empty(tier->announce_events));
TR_ASSERT(tier->currentTracker() != nullptr);
auto const now = tr_time();
tr_torrent* tor = tier->tor;
auto const event = tier_announce_event_pull(tier);
auto const req = create_announce_request(announcer, tor, tier, event);
tier->isAnnouncing = true;
tier->lastAnnounceStartTime = now;
auto tier_id = tier->id;
auto is_running_on_success = tor->is_running();
announcer->announce(
req,
[session = announcer->session, announcer, tier_id, event, is_running_on_success](tr_announce_response const& response)
{
if (session->announcer_)
{
announcer->onAnnounceDone(tier_id, event, is_running_on_success, response);
}
});
}
void scrapeAndAnnounceMore(tr_announcer_impl* announcer)
{
auto const now = tr_time();
/* build a list of tiers that need to be announced */
auto announce_me = std::vector<tr_tier*>{};
auto scrape_me = std::vector<tr_tier*>{};
for (auto* const tor : announcer->session->torrents())
{
for (auto& tier : tor->torrent_announcer->tiers)
{
if (tier.needsToAnnounce(now))
{
announce_me.push_back(&tier);
}
if (tier.needsToScrape(now))
{
scrape_me.push_back(&tier);
}
}
}
/* First, scrape what we can. We handle scrapes first because
* we can work through that queue much faster than announces
* (thanks to multiscrape) _and_ the scrape responses will tell
* us which swarms are interesting and should be announced next. */
multiscrape(announcer, scrape_me);
/* Second, announce what we can. If there aren't enough slots
* available, use compareAnnounceTiers to prioritize. */
if (announce_me.size() > MaxAnnouncesPerUpkeep)
{
std::partial_sort(
std::begin(announce_me),
std::begin(announce_me) + MaxAnnouncesPerUpkeep,
std::end(announce_me),
[](auto const* a, auto const* b) { return compareAnnounceTiers(a, b) < 0; });
announce_me.resize(MaxAnnouncesPerUpkeep);
}
for (auto*& tier : announce_me)
{
tr_logAddTraceTier(tier, "Announcing to tracker");
tierAnnounce(announcer, tier);
}
}
} // namespace upkeep_helpers
} // namespace
void tr_announcer_impl::upkeep()
{
using namespace upkeep_helpers;
auto const lock = session->unique_lock();
// maybe send out some "stopped" messages for closed torrents
flushCloseMessages();
// maybe kick off some scrapes / announces whose time has come
if (!is_shutting_down_)
{
scrapeAndAnnounceMore(this);
}
announcer_udp_.upkeep();
}
// ---
namespace
{
namespace tracker_view_helpers
{
[[nodiscard]] auto trackerView(tr_torrent const& tor, size_t tier_index, tr_tier const& tier, tr_tracker const& tracker)
{
auto const& announce = tracker.announce_parsed;
auto const now = tr_time();
auto view = tr_tracker_view{};
*fmt::format_to_n(
std::data(view.host_and_port),
std::size(view.host_and_port) - 1U,
"{:s}:{:d}",
announce.host,
announce.port)
.out = '\0';
*fmt::format_to_n(std::data(view.sitename), std::size(view.sitename) - 1U, "{:s}", announce.sitename).out = '\0';
view.announce = tracker.announce_url.c_str();
view.scrape = tracker.scrape_info == nullptr ? "" : tracker.scrape_info->scrape_url.c_str();
view.id = tracker.id;
view.tier = tier_index;
view.isBackup = &tracker != tier.currentTracker();
view.lastScrapeStartTime = tier.lastScrapeStartTime;
view.seederCount = tracker.seeder_count().value_or(-1);
view.leecherCount = tracker.leecher_count().value_or(-1);
view.downloadCount = tracker.download_count().value_or(-1);
if (view.isBackup)
{
view.scrapeState = TR_TRACKER_INACTIVE;
view.announceState = TR_TRACKER_INACTIVE;
view.nextScrapeTime = 0;
view.nextAnnounceTime = 0;
}
else
{
view.hasScraped = tier.lastScrapeTime != 0;
if (view.hasScraped)
{
view.lastScrapeTime = tier.lastScrapeTime;
view.lastScrapeSucceeded = tier.lastScrapeSucceeded;
view.lastScrapeTimedOut = tier.lastScrapeTimedOut;
auto& buf = view.lastScrapeResult;
*fmt::format_to_n(buf, sizeof(buf) - 1, "{:s}", tier.last_scrape_str).out = '\0';
}
if (tier.isScraping)
{
view.scrapeState = TR_TRACKER_ACTIVE;
}
else if (tier.scrapeAt == 0)
{
view.scrapeState = TR_TRACKER_INACTIVE;
}
else if (tier.scrapeAt > now)
{
view.scrapeState = TR_TRACKER_WAITING;
view.nextScrapeTime = tier.scrapeAt;
}
else
{
view.scrapeState = TR_TRACKER_QUEUED;
}
view.lastAnnounceStartTime = tier.lastAnnounceStartTime;
view.hasAnnounced = tier.lastAnnounceTime != 0;
if (view.hasAnnounced)
{
view.lastAnnounceTime = tier.lastAnnounceTime;
view.lastAnnounceSucceeded = tier.lastAnnounceSucceeded;
view.lastAnnounceTimedOut = tier.lastAnnounceTimedOut;
view.lastAnnouncePeerCount = tier.lastAnnouncePeerCount;
auto& buf = view.lastAnnounceResult;
*fmt::format_to_n(buf, sizeof(buf) - 1, "{:s}", tier.last_announce_str).out = '\0';
}
if (tier.isAnnouncing)
{
view.announceState = TR_TRACKER_ACTIVE;
}
else if (!tor.is_running() || tier.announceAt == 0)
{
view.announceState = TR_TRACKER_INACTIVE;
}
else if (tier.announceAt > now)
{
view.announceState = TR_TRACKER_WAITING;
view.nextAnnounceTime = tier.announceAt;
}
else
{
view.announceState = TR_TRACKER_QUEUED;
}
}
return view;
}
} // namespace tracker_view_helpers
} // namespace
size_t tr_announcerTrackerCount(tr_torrent const* tor)
{
TR_ASSERT(tr_isTorrent(tor));
TR_ASSERT(tor->torrent_announcer != nullptr);
auto const& tiers = tor->torrent_announcer->tiers;
return std::accumulate(
std::begin(tiers),
std::end(tiers),
size_t{},
[](size_t acc, auto const& cur) { return acc + std::size(cur.trackers); });
}
tr_tracker_view tr_announcerTracker(tr_torrent const* tor, size_t nth)
{
using namespace tracker_view_helpers;
TR_ASSERT(tr_isTorrent(tor));
TR_ASSERT(tor->torrent_announcer != nullptr);
auto tier_index = size_t{ 0 };
auto tracker_index = size_t{ 0 };
for (auto const& tier : tor->torrent_announcer->tiers)
{
for (auto const& tracker : tier.trackers)
{
if (tracker_index == nth)
{
return trackerView(*tor, tier_index, tier, tracker);
}
++tracker_index;
}
++tier_index;
}
TR_ASSERT(false);
return {};
}
// ---
// called after the torrent's announceList was rebuilt --
// so announcer needs to update the tr_tier / tr_trackers to match
void tr_announcer_impl::resetTorrent(tr_torrent* tor)
{
using namespace announce_helpers;
// make a new tr_announcer_tier
auto* const older = tor->torrent_announcer;
tor->torrent_announcer = new tr_torrent_announcer{ this, tor };
auto* const newer = tor->torrent_announcer;
// copy the tracker counts into the new replacementa
if (older != nullptr)
{
for (auto& new_tier : newer->tiers)
{
for (auto& new_tracker : new_tier.trackers)
{
tr_tier const* old_tier = nullptr;
tr_tracker const* old_tracker = nullptr;
if (older->findTracker(new_tracker.announce_url, &old_tier, &old_tracker))
{
new_tracker.set_seeder_count(old_tracker->seeder_count());
new_tracker.set_leecher_count(old_tracker->leecher_count());
new_tracker.set_download_count(old_tracker->download_count());
new_tracker.set_downloader_count(old_tracker->downloader_count());
new_tier.announce_events = old_tier->announce_events;
new_tier.announce_event_priority = old_tier->announce_event_priority;
auto const* const old_current = old_tier->currentTracker();
new_tier.current_tracker_index_ = old_current == nullptr ? std::nullopt :
new_tier.indexOf(old_current->announce_url);
}
}
}
}
// kickstart any tiers that didn't get started
if (tor->is_running())
{
auto const now = tr_time();
for (auto& tier : newer->tiers)
{
if (!tier.current_tracker_index_)
{
tier.useNextTracker();
tier_announce_event_push(&tier, TR_ANNOUNCE_EVENT_STARTED, now);
}
}
}
delete older;
}