refactor: dedicated class for torrent queue (#7332)

* Revert "feat: save queue order between sessions (#6753)"

This reverts commit 4db50dae10.

* refactor: new torrent queue class

* refactor: replace queue code with new class

* test: new tests for new class

* feat: store and load queue order across sessions

* build: xcode

* refactor: use set_difference instead of unordered_set

* code review: use `tr_torrent_id_t` as key

* fix: don't overflow when moving up/down

---------

Co-authored-by: Dzmitry Neviadomski <nevack.d@gmail.com>
This commit is contained in:
Yat Ho
2025-03-10 08:08:50 +08:00
committed by GitHub
parent 4e7fc81975
commit 47eb4ee2bc
16 changed files with 504 additions and 181 deletions

View File

@@ -454,6 +454,8 @@
ED8A16412735A8AA000D61F9 /* peer-mgr-wishlist.h in Headers */ = {isa = PBXBuildFile; fileRef = ED8A163D2735A8AA000D61F9 /* peer-mgr-wishlist.h */; };
ED8A16422735A8AA000D61F9 /* peer-mgr-wishlist.cc in Sources */ = {isa = PBXBuildFile; fileRef = ED8A163E2735A8AA000D61F9 /* peer-mgr-wishlist.cc */; };
ED9862972B979AA2002F3035 /* Utils.mm in Sources */ = {isa = PBXBuildFile; fileRef = ED9862962B979AA2002F3035 /* Utils.mm */; };
EDBA61FF2D4180D5001470F8 /* torrent-queue.h in Headers */ = {isa = PBXBuildFile; fileRef = EDBA61FD2D4180D5001470F8 /* torrent-queue.h */; };
EDBA62002D4180D5001470F8 /* torrent-queue.cc in Sources */ = {isa = PBXBuildFile; fileRef = EDBA61FE2D4180D5001470F8 /* torrent-queue.cc */; };
EDBAAC8C29E486BC00D9495F /* ip-cache.h in Headers */ = {isa = PBXBuildFile; fileRef = EDBAAC8B29E486BC00D9495F /* ip-cache.h */; };
EDBAAC8E29E486C200D9495F /* ip-cache.cc in Sources */ = {isa = PBXBuildFile; fileRef = EDBAAC8D29E486C200D9495F /* ip-cache.cc */; };
EDBDFA9E25AFCCA60093D9C1 /* evutil_time.c in Sources */ = {isa = PBXBuildFile; fileRef = EDBDFA9D25AFCCA60093D9C1 /* evutil_time.c */; };
@@ -1372,6 +1374,8 @@
ED8A163E2735A8AA000D61F9 /* peer-mgr-wishlist.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "peer-mgr-wishlist.cc"; sourceTree = "<group>"; };
ED9862952B979AA2002F3035 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Utils.h; sourceTree = "<group>"; };
ED9862962B979AA2002F3035 /* Utils.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = Utils.mm; sourceTree = "<group>"; };
EDBA61FD2D4180D5001470F8 /* torrent-queue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "torrent-queue.h"; sourceTree = "<group>"; };
EDBA61FE2D4180D5001470F8 /* torrent-queue.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "torrent-queue.cc"; sourceTree = "<group>"; };
EDBAAC8B29E486BC00D9495F /* ip-cache.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; fileEncoding = 4; path = "ip-cache.h"; sourceTree = "<group>"; };
EDBAAC8D29E486C200D9495F /* ip-cache.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "ip-cache.cc"; sourceTree = "<group>"; };
EDBDFA9D25AFCCA60093D9C1 /* evutil_time.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = evutil_time.c; sourceTree = "<group>"; };
@@ -1903,6 +1907,8 @@
4D8017E910BBC073008A4AF2 /* torrent-magnet.h */,
0A89346B736DBCF81F3A4851 /* torrent-metainfo.cc */,
0A89346B736DBCF81F3A4853 /* torrent-metainfo.h */,
EDBA61FD2D4180D5001470F8 /* torrent-queue.h */,
EDBA61FE2D4180D5001470F8 /* torrent-queue.cc */,
BEFC1DF90C07861A00B0BB3C /* torrent.cc */,
A29DF8B80DB2544C00D04E5A /* torrent.h */,
2B9BA6C508B488FE586A0AB1 /* torrents.cc */,
@@ -2407,6 +2413,7 @@
4D36BA780CA2F00800A63CA5 /* peer-mgr.h in Headers */,
4D36BA7A0CA2F00800A63CA5 /* peer-msgs.h in Headers */,
C11DEA171FCD31C0009E22B9 /* subprocess.h in Headers */,
EDBA61FF2D4180D5001470F8 /* torrent-queue.h in Headers */,
A25D2CBE0CF4C73E0096A262 /* stats.h in Headers */,
C1033E0A1A3279B800EF44D8 /* crypto-utils.h in Headers */,
C17740D6273A002C00E455D2 /* web-utils.h in Headers */,
@@ -3213,6 +3220,7 @@
C17740D5273A002C00E455D2 /* web-utils.cc in Sources */,
A2679294130E00A000CB7464 /* tr-utp.cc in Sources */,
A23F29A2132A447400E9A83B /* announcer-http.cc in Sources */,
EDBA62002D4180D5001470F8 /* torrent-queue.cc in Sources */,
C1FEE5791C3223CC00D62832 /* watchdir-kqueue.cc in Sources */,
A2AA9BE1132CAC8E00FA131E /* announcer-udp.cc in Sources */,
A25BFD69167BED3B0039D1AA /* variant-benc.cc in Sources */,

View File

@@ -134,6 +134,8 @@ target_sources(${TR_NAME}
torrent-magnet.h
torrent-metainfo.cc
torrent-metainfo.h
torrent-queue.cc
torrent-queue.h
torrent.cc
torrent.h
torrents.cc

View File

@@ -170,7 +170,6 @@ auto constexpr MyStatic = std::array<std::string_view, TR_N_KEYS>{
"isStalled"sv,
"isUTP"sv,
"isUploadingTo"sv,
"is_queued"sv,
"labels"sv,
"lastAnnouncePeerCount"sv,
"lastAnnounceResult"sv,

View File

@@ -172,7 +172,6 @@ enum
TR_KEY_isStalled,
TR_KEY_isUTP,
TR_KEY_isUploadingTo,
TR_KEY_is_queued,
TR_KEY_labels,
TR_KEY_lastAnnouncePeerCount,
TR_KEY_lastAnnounceResult,

View File

@@ -412,44 +412,6 @@ tr_resume::fields_t loadFilenames(tr_variant* dict, tr_torrent* tor)
// ---
void saveQueueState(tr_variant* dict, tr_torrent const* tor)
{
auto* const map = dict->get_if<tr_variant::Map>();
if (map == nullptr)
{
return;
}
map->try_emplace(TR_KEY_queuePosition, tor->queue_position());
map->try_emplace(TR_KEY_is_queued, tor->is_queued(tor->queue_direction()));
}
auto loadQueueState(tr_variant* dict, tr_torrent* tor, tr_torrent::ResumeHelper& helper)
{
auto ret = tr_resume::fields_t{};
auto const* const map = dict->get_if<tr_variant::Map>();
if (map == nullptr)
{
return ret;
}
if (auto val = map->value_if<int64_t>(TR_KEY_queuePosition))
{
helper.load_queue_position(*val);
ret = tr_resume::QueueState;
}
if (auto val = map->value_if<bool>(TR_KEY_is_queued))
{
tor->set_is_queued(*val);
ret = tr_resume::QueueState;
}
return ret;
}
// ---
void bitfieldToRaw(tr_bitfield const& b, tr_variant* benc)
{
if (b.has_none() || std::empty(b))
@@ -830,11 +792,6 @@ tr_resume::fields_t load_from_file(tr_torrent* tor, tr_torrent::ResumeHelper& he
fields_loaded |= loadGroup(&top, tor);
}
if ((fields_to_load & tr_resume::QueueState) != 0)
{
fields_loaded |= loadQueueState(&top, tor, helper);
}
return fields_loaded;
}
@@ -956,7 +913,6 @@ void save(tr_torrent* const tor, tr_torrent::ResumeHelper const& helper)
saveName(&top, tor);
saveLabels(&top, tor);
saveGroup(&top, tor);
saveQueueState(&top, tor);
auto serde = tr_variant_serde::benc();
if (!serde.to_file(top, tor->resume_file()))

View File

@@ -45,7 +45,6 @@ auto inline constexpr Name = fields_t{ 1 << 21 };
auto inline constexpr Labels = fields_t{ 1 << 22 };
auto inline constexpr Group = fields_t{ 1 << 23 };
auto inline constexpr SequentialDownload = fields_t{ 1 << 24 };
auto inline constexpr QueueState = fields_t{ 1 << 25 };
auto inline constexpr All = ~fields_t{ 0 };

View File

@@ -245,6 +245,14 @@ void tr_session::DhtMediator::add_pex(tr_sha1_digest_t const& info_hash, tr_pex
// ---
std::string tr_session::QueueMediator::store_filename(tr_torrent_id_t id) const
{
auto const* const tor = session_.torrents().get(id);
return tor != nullptr ? tor->store_filename() : std::string{};
}
// ---
bool tr_session::LpdMediator::onPeerFound(std::string_view info_hash_str, tr_address address, tr_port port)
{
auto const digest = tr_sha1_from_string(info_hash_str);
@@ -1336,6 +1344,8 @@ void tr_session::closeImplPart1(std::promise<void>* closed_promise, std::chrono:
bound_ipv6_.reset();
bound_ipv4_.reset();
torrent_queue().to_file();
// Close the torrents in order of most active to least active
// so that the most important announce=stopped events are
// fired out first...
@@ -1424,32 +1434,58 @@ namespace
{
namespace load_torrents_helpers
{
auto get_remaining_files(std::string_view folder, std::vector<std::string>& queue_order)
{
auto files = tr_sys_dir_get_files(folder);
auto ret = std::vector<std::string>{};
ret.reserve(std::size(files));
std::sort(std::begin(queue_order), std::end(queue_order));
std::sort(std::begin(files), std::end(files));
std::set_difference(
std::begin(files),
std::end(files),
std::begin(queue_order),
std::end(queue_order),
std::back_inserter(ret));
return ret;
}
void session_load_torrents(tr_session* session, tr_ctor* ctor, std::promise<size_t>* loaded_promise)
{
auto n_torrents = size_t{};
auto const& folder = session->torrentDir();
for (auto const& name : tr_sys_dir_get_files(folder, [](auto name) { return tr_strv_ends_with(name, ".torrent"sv); }))
auto load_func = [&folder, &n_torrents, ctor, buf = std::vector<char>{}](std::string_view name) mutable
{
auto const path = tr_pathbuf{ folder, '/', name };
if (ctor->set_metainfo_from_file(path.sv()) && tr_torrentNew(ctor, nullptr) != nullptr)
if (tr_strv_ends_with(name, ".torrent"sv))
{
++n_torrents;
auto const path = tr_pathbuf{ folder, '/', name };
if (ctor->set_metainfo_from_file(path.sv()) && tr_torrentNew(ctor, nullptr) != nullptr)
{
++n_torrents;
}
}
else if (tr_strv_ends_with(name, ".magnet"sv))
{
auto const path = tr_pathbuf{ folder, '/', name };
if (tr_file_read(path, buf) &&
ctor->set_metainfo_from_magnet_link(std::string_view{ std::data(buf), std::size(buf) }, nullptr) &&
tr_torrentNew(ctor, nullptr) != nullptr)
{
++n_torrents;
}
}
};
auto queue_order = session->torrent_queue().from_file();
for (auto const& filename : queue_order)
{
load_func(filename);
}
auto buf = std::vector<char>{};
for (auto const& name : tr_sys_dir_get_files(folder, [](auto name) { return tr_strv_ends_with(name, ".magnet"sv); }))
for (auto const& filename : get_remaining_files(folder, queue_order))
{
auto const path = tr_pathbuf{ folder, '/', name };
if (tr_file_read(path, buf) &&
ctor->set_metainfo_from_magnet_link(std::string_view{ std::data(buf), std::size(buf) }, nullptr) &&
tr_torrentNew(ctor, nullptr) != nullptr)
{
++n_torrents;
}
load_func(filename);
}
if (n_torrents != 0U)
@@ -2130,6 +2166,7 @@ void tr_session::addIncoming(tr_peer_socket&& socket)
void tr_session::addTorrent(tr_torrent* tor)
{
tor->init_id(torrents().add(tor));
torrent_queue_.add(tor->id());
tr_peerMgrAddTorrent(peer_mgr_.get(), tor);
}

View File

@@ -57,6 +57,7 @@
#include "libtransmission/settings.h"
#include "libtransmission/stats.h"
#include "libtransmission/timer.h"
#include "libtransmission/torrent-queue.h"
#include "libtransmission/torrents.h"
#include "libtransmission/tr-assert.h"
#include "libtransmission/tr-dht.h"
@@ -230,6 +231,25 @@ private:
tr_session& session_;
};
class QueueMediator final : public tr_torrent_queue::Mediator
{
public:
explicit QueueMediator(tr_session& session) noexcept
: session_{ session }
{
}
[[nodiscard]] std::string config_dir() const override
{
return session_.configDir();
}
[[nodiscard]] std::string store_filename(tr_torrent_id_t id) const override;
private:
tr_session& session_;
};
class WebMediator final : public tr_web::Mediator
{
public:
@@ -525,16 +545,26 @@ public:
return session_thread_->event_base();
}
[[nodiscard]] constexpr auto& torrents()
[[nodiscard]] constexpr tr_torrents& torrents()
{
return torrents_;
}
[[nodiscard]] constexpr auto const& torrents() const
[[nodiscard]] constexpr tr_torrents const& torrents() const
{
return torrents_;
}
[[nodiscard]] constexpr auto& torrent_queue()
{
return torrent_queue_;
}
[[nodiscard]] constexpr auto const& torrent_queue() const
{
return torrent_queue_;
}
[[nodiscard]] auto unique_lock() const
{
return std::unique_lock(session_mutex_);
@@ -547,7 +577,7 @@ public:
// paths
[[nodiscard]] constexpr auto const& configDir() const noexcept
[[nodiscard]] constexpr std::string const& configDir() const noexcept
{
return config_dir_;
}
@@ -1263,6 +1293,9 @@ private:
libtransmission::Blocklists blocklists_;
QueueMediator torrent_queue_mediator_{ *this };
tr_torrent_queue torrent_queue_{ torrent_queue_mediator_ };
private:
/// other fields

View File

@@ -663,9 +663,9 @@ std::string tr_torrent_metainfo::make_filename(
BasenameFormat format,
std::string_view suffix)
{
// `${dirname}/${name}.${info_hash}${suffix}`
// `${dirname}/${info_hash}${suffix}`
auto filename = tr_pathbuf{ dirname, '/' };
// `[${dirname}/]${name}.${info_hash}${suffix}`
// `[${dirname}/]${info_hash}${suffix}`
auto filename = std::empty(dirname) ? tr_pathbuf{} : tr_pathbuf{ dirname, '/' };
if (format == BasenameFormat::Hash)
{
filename.append(info_hash_string);

View File

@@ -167,17 +167,17 @@ public:
// UTILS
[[nodiscard]] auto torrent_file(std::string_view torrent_dir) const
[[nodiscard]] auto torrent_file(std::string_view torrent_dir = {}) const
{
return make_filename(torrent_dir, name(), info_hash_string(), BasenameFormat::Hash, ".torrent");
}
[[nodiscard]] auto magnet_file(std::string_view torrent_dir) const
[[nodiscard]] auto magnet_file(std::string_view torrent_dir = {}) const
{
return make_filename(torrent_dir, name(), info_hash_string(), BasenameFormat::Hash, ".magnet");
}
[[nodiscard]] auto resume_file(std::string_view resume_dir) const
[[nodiscard]] auto resume_file(std::string_view resume_dir = {}) const
{
return make_filename(resume_dir, name(), info_hash_string(), BasenameFormat::Hash, ".resume");
}

View File

@@ -0,0 +1,134 @@
// 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 <string>
#include <string_view>
#include <vector>
#include "libtransmission/torrent-queue.h"
#include "libtransmission/tr-strbuf.h"
#include "libtransmission/variant.h"
namespace
{
using namespace std::literals;
[[nodiscard]] auto get_file_path(std::string_view config_dir) noexcept
{
return tr_pathbuf{ config_dir, '/', "queue.json"sv };
}
} // namespace
size_t tr_torrent_queue::add(tr_torrent_id_t const id)
{
queue_.push_back(id);
return std::size(queue_) - 1U;
}
void tr_torrent_queue::remove(tr_torrent_id_t const id)
{
auto const uid = static_cast<size_t>(id);
auto const pos = uid < std::size(pos_cache_) ? pos_cache_[uid] : 0U;
if (pos < std::size(queue_) && queue_[pos] == id)
{
queue_.erase(std::begin(queue_) + pos);
}
else
{
auto const remove_it = std::remove(std::begin(queue_), std::end(queue_), id);
queue_.erase(remove_it, std::end(queue_));
}
}
size_t tr_torrent_queue::get_pos(tr_torrent_id_t const id)
{
auto const uid = static_cast<size_t>(id);
if (auto n_cache = std::size(pos_cache_);
uid >= n_cache || pos_cache_[uid] >= std::size(queue_) || id != queue_[pos_cache_[uid]])
{
auto const begin = std::begin(queue_);
auto const end = std::end(queue_);
auto it = std::find(begin, end, id);
if (it == end)
{
return MaxQueuePosition;
}
pos_cache_.resize(std::max(uid + 1U, n_cache));
pos_cache_[uid] = it - begin;
}
return pos_cache_[uid];
}
void tr_torrent_queue::set_pos(tr_torrent_id_t const id, size_t new_pos)
{
auto const old_pos = get_pos(id);
auto const n_queue = std::size(queue_);
if (old_pos >= n_queue || queue_[old_pos] != id)
{
return;
}
new_pos = std::min(new_pos, n_queue - 1U);
if (old_pos == new_pos)
{
return;
}
auto const begin = std::begin(queue_);
auto const old_it = std::next(begin, old_pos);
auto const next_it = std::next(old_it);
auto const new_it = std::next(begin, new_pos);
if (old_pos > new_pos)
{
std::rotate(new_it, old_it, next_it);
}
else
{
std::rotate(old_it, next_it, std::next(new_it));
}
}
bool tr_torrent_queue::to_file() const
{
auto vec = tr_variant::Vector{};
vec.reserve(std::size(queue_));
for (auto const id : queue_)
{
vec.emplace_back(mediator_.store_filename(id));
}
return tr_variant_serde::json().to_file(std::move(vec), get_file_path(mediator_.config_dir()));
}
std::vector<std::string> tr_torrent_queue::from_file()
{
auto top = tr_variant_serde::json().parse_file(get_file_path(mediator_.config_dir()));
if (!top)
{
return {};
}
auto const* const vec = top->get_if<tr_variant::Vector>();
if (vec == nullptr)
{
return {};
}
auto ret = std::vector<std::string>{};
ret.reserve(std::size(*vec));
for (auto const& var : *vec)
{
if (auto file = var.value_if<std::string_view>(); file)
{
ret.emplace_back(*file);
}
}
return ret;
}

View File

@@ -0,0 +1,56 @@
// 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.
#pragma once
#ifndef __TRANSMISSION__
#error only libtransmission should #include this header.
#endif
#include <cstddef>
#include <string>
#include <string_view>
#include <vector>
#include "libtransmission/transmission.h"
class tr_torrent_queue
{
public:
struct Mediator
{
virtual ~Mediator() = default;
[[nodiscard]] virtual std::string config_dir() const = 0;
[[nodiscard]] virtual std::string store_filename(tr_torrent_id_t id) const = 0;
};
explicit tr_torrent_queue(Mediator const& mediator)
: mediator_{ mediator }
{
}
tr_torrent_queue(tr_torrent_queue const&) = delete;
tr_torrent_queue(tr_torrent_queue&&) = delete;
tr_torrent_queue& operator=(tr_torrent_queue const&) = delete;
tr_torrent_queue& operator=(tr_torrent_queue&&) = delete;
size_t add(tr_torrent_id_t id);
void remove(tr_torrent_id_t id);
[[nodiscard]] size_t get_pos(tr_torrent_id_t id);
void set_pos(tr_torrent_id_t id, size_t new_pos);
bool to_file() const;
[[nodiscard]] std::vector<std::string> from_file();
static auto constexpr MinQueuePosition = size_t{};
static auto constexpr MaxQueuePosition = ~size_t{};
private:
std::vector<tr_torrent_id_t> queue_;
std::vector<size_t> pos_cache_;
Mediator const& mediator_;
};

View File

@@ -6,10 +6,8 @@
#include <algorithm>
#include <array>
#include <cerrno> // EINVAL
#include <climits> /* INT_MAX */
#include <cstddef> // size_t
#include <ctime>
#include <limits>
#include <map>
#include <sstream>
#include <string>
@@ -464,132 +462,59 @@ void tr_torrent::stop_if_seed_limit_reached()
// --- Queue
namespace
{
namespace queue_helpers
{
constexpr auto MinQueuePosition = std::numeric_limits<size_t>::min();
constexpr auto MaxQueuePosition = std::numeric_limits<size_t>::max();
#ifdef TR_ENABLE_ASSERTS
[[nodiscard]] bool torrents_are_sorted_by_queue_position(std::vector<tr_torrent*> torrents)
{
std::sort(std::begin(torrents), std::end(torrents), tr_torrent::CompareQueuePosition);
for (size_t idx = 0, end_idx = std::size(torrents); idx < end_idx; ++idx)
{
if (torrents[idx]->queue_position() != idx)
{
return false;
}
}
return true;
}
#endif
} // namespace queue_helpers
} // namespace
size_t tr_torrentGetQueuePosition(tr_torrent const* tor)
{
return tor->queue_position();
}
void tr_torrent::set_unique_queue_position(size_t const new_pos)
{
using namespace queue_helpers;
auto max_pos = size_t{};
auto const old_pos = queue_position_;
auto& torrents = session->torrents();
for (auto* const walk : torrents)
{
if (walk == this)
{
continue;
}
if ((old_pos < new_pos) && (old_pos < walk->queue_position_) && (walk->queue_position_ <= new_pos))
{
--walk->queue_position_;
walk->mark_changed();
walk->set_dirty();
}
if ((old_pos > new_pos) && (new_pos <= walk->queue_position_) && (walk->queue_position_ < old_pos))
{
++walk->queue_position_;
walk->mark_changed();
walk->set_dirty();
}
max_pos = std::max(max_pos, walk->queue_position_);
}
queue_position_ = std::min(new_pos, max_pos + 1);
mark_changed();
set_dirty();
TR_ASSERT(torrents_are_sorted_by_queue_position(torrents.get_all()));
}
void tr_torrentSetQueuePosition(tr_torrent* tor, size_t queue_position)
{
tor->set_unique_queue_position(queue_position);
tor->set_queue_position(queue_position);
}
void tr_torrentsQueueMoveTop(tr_torrent* const* torrents_in, size_t torrent_count)
{
using namespace queue_helpers;
auto torrents = std::vector<tr_torrent*>(torrents_in, torrents_in + torrent_count);
std::sort(std::rbegin(torrents), std::rend(torrents), tr_torrent::CompareQueuePosition);
for (auto* const tor : torrents)
{
tor->set_unique_queue_position(MinQueuePosition);
tor->set_queue_position(tr_torrent_queue::MinQueuePosition);
}
}
void tr_torrentsQueueMoveUp(tr_torrent* const* torrents_in, size_t torrent_count)
{
using namespace queue_helpers;
auto torrents = std::vector<tr_torrent*>(torrents_in, torrents_in + torrent_count);
std::sort(std::begin(torrents), std::end(torrents), tr_torrent::CompareQueuePosition);
for (auto* const tor : torrents)
{
if (auto const pos = tor->queue_position(); pos > MinQueuePosition)
if (auto const pos = tor->queue_position(); pos > tr_torrent_queue::MinQueuePosition)
{
tor->set_unique_queue_position(pos - 1U);
tor->set_queue_position(pos - 1U);
}
}
}
void tr_torrentsQueueMoveDown(tr_torrent* const* torrents_in, size_t torrent_count)
{
using namespace queue_helpers;
auto torrents = std::vector<tr_torrent*>(torrents_in, torrents_in + torrent_count);
std::sort(std::rbegin(torrents), std::rend(torrents), tr_torrent::CompareQueuePosition);
for (auto* const tor : torrents)
{
if (auto const pos = tor->queue_position(); pos < MaxQueuePosition)
if (auto const pos = tor->queue_position(); pos < tr_torrent_queue::MaxQueuePosition)
{
tor->set_unique_queue_position(pos + 1U);
tor->set_queue_position(pos + 1U);
}
}
}
void tr_torrentsQueueMoveBottom(tr_torrent* const* torrents_in, size_t torrent_count)
{
using namespace queue_helpers;
auto torrents = std::vector<tr_torrent*>(torrents_in, torrents_in + torrent_count);
std::sort(std::begin(torrents), std::end(torrents), tr_torrent::CompareQueuePosition);
for (auto* const tor : torrents)
{
tor->set_unique_queue_position(MaxQueuePosition);
tor->set_queue_position(tr_torrent_queue::MaxQueuePosition);
}
}
@@ -613,8 +538,6 @@ bool removeTorrentFile(char const* filename, void* /*user_data*/, tr_error* erro
void freeTorrent(tr_torrent* tor)
{
using namespace queue_helpers;
auto const lock = tor->unique_lock();
TR_ASSERT(!tor->is_running());
@@ -629,9 +552,7 @@ void freeTorrent(tr_torrent* tor)
if (!session->isClosing())
{
// move the torrent being freed to the end of the queue so that
// all the torrents queued after it will move up one position
tor->set_unique_queue_position(queue_helpers::MaxQueuePosition);
session->torrent_queue().remove(tor->id());
}
delete tor;
@@ -970,8 +891,6 @@ void tr_torrent::init(tr_ctor const& ctor)
auto const now_sec = tr_time();
queue_position_ = std::size(session->torrents());
on_metainfo_updated();
if (auto dir = ctor.download_dir(TR_FORCE); !std::empty(dir))
@@ -1058,11 +977,11 @@ void tr_torrent::init(tr_ctor const& ctor)
[](auto mtime) { return mtime > 0; });
}
auto const filename = has_metainfo() ? torrent_file() : magnet_file();
auto const file_path = store_file();
// if we don't have a local .torrent or .magnet file already,
// assume the torrent is new
bool const is_new_torrent = !tr_sys_path_exists(filename);
bool const is_new_torrent = !tr_sys_path_exists(file_path);
if (is_new_torrent)
{
@@ -1070,19 +989,19 @@ void tr_torrent::init(tr_ctor const& ctor)
if (has_metainfo()) // torrent file
{
ctor.save(filename, &error);
ctor.save(file_path, &error);
}
else // magnet link
{
auto const magnet_link = magnet();
tr_file_save(filename, magnet_link, &error);
tr_file_save(file_path, magnet_link, &error);
}
if (error)
{
this->error().set_local_error(fmt::format(
_("Couldn't save '{path}': {error} ({error_code})"),
fmt::arg("path", filename),
fmt::arg("path", file_path),
fmt::arg("error", error.message()),
fmt::arg("error_code", error.code())));
}
@@ -2737,13 +2656,6 @@ void tr_torrent::ResumeHelper::load_incomplete_dir(std::string_view const dir) n
// ---
void tr_torrent::ResumeHelper::load_queue_position(size_t pos) noexcept
{
tor_.queue_position_ = pos;
}
// ---
void tr_torrent::ResumeHelper::load_start_when_stable(bool const val) noexcept
{
tor_.start_when_stable_ = val;

View File

@@ -76,7 +76,6 @@ struct tr_torrent
void load_date_done(time_t when) noexcept;
void load_download_dir(std::string_view dir) noexcept;
void load_incomplete_dir(std::string_view dir) noexcept;
void load_queue_position(size_t pos) noexcept;
void load_seconds_downloading_before_current_start(time_t when) noexcept;
void load_seconds_seeding_before_current_start(time_t when) noexcept;
void load_start_when_stable(bool val) noexcept;
@@ -560,6 +559,21 @@ struct tr_torrent
return metainfo_.date_created();
}
[[nodiscard]] auto torrent_filename() const
{
return metainfo_.torrent_file();
}
[[nodiscard]] auto magnet_filename() const
{
return metainfo_.magnet_file();
}
[[nodiscard]] auto store_filename() const
{
return has_metainfo() ? torrent_filename() : magnet_filename();
}
[[nodiscard]] auto torrent_file() const
{
return metainfo_.torrent_file(session->torrentDir());
@@ -570,6 +584,11 @@ struct tr_torrent
return metainfo_.magnet_file(session->torrentDir());
}
[[nodiscard]] auto store_file() const
{
return has_metainfo() ? torrent_file() : magnet_file();
}
[[nodiscard]] auto resume_file() const
{
return metainfo_.resume_file(session->resumeDir());
@@ -941,18 +960,21 @@ struct tr_torrent
// --- queue position
[[nodiscard]] constexpr auto queue_position() const noexcept
[[nodiscard]] auto queue_position() const noexcept
{
return queue_position_;
return session->torrent_queue().get_pos(id());
}
void set_unique_queue_position(size_t new_pos);
void set_queue_position(size_t new_pos)
{
session->torrent_queue().set_pos(id(), new_pos);
}
static constexpr struct
{
constexpr bool operator()(tr_torrent const* a, tr_torrent const* b) const noexcept
bool operator()(tr_torrent const* a, tr_torrent const* b) const noexcept
{
return a->queue_position_ < b->queue_position_;
return a->queue_position() < b->queue_position();
}
} CompareQueuePosition{};
@@ -1344,8 +1366,6 @@ private:
*/
tr_peer_id_t peer_id_ = tr_peerIdInit();
size_t queue_position_ = 0;
time_t date_active_ = 0;
time_t date_added_ = 0;
time_t date_changed_ = 0;

View File

@@ -49,6 +49,7 @@ target_sources(libtransmission-test
torrent-files-test.cc
torrent-magnet-test.cc
torrent-metainfo-test.cc
torrent-queue-test.cc
torrents-test.cc
tr-peer-info-test.cc
utils-test.cc

View File

@@ -0,0 +1,167 @@
// 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 <fstream>
#include <map>
#include <memory>
#include <string_view>
#include <vector>
#include <libtransmission/torrent-queue.h>
#include <libtransmission/torrent.h>
#include "gtest/gtest.h"
#include "test-fixtures.h"
using namespace std::literals;
struct TorrentQueueTest : public libtransmission::test::SandboxedTest
{
class MockMediator final : public tr_torrent_queue::Mediator
{
public:
explicit MockMediator(TorrentQueueTest const& test)
: test_{ test }
{
}
[[nodiscard]] std::string config_dir() const override
{
return test_.sandboxDir();
}
[[nodiscard]] std::string store_filename(tr_torrent_id_t id) const override
{
if (auto it = test_.torrents_.find(id); it != std::end(test_.torrents_))
{
return it->second.store_filename();
}
return {};
}
private:
TorrentQueueTest const& test_;
};
std::map<tr_torrent_id_t, tr_torrent const&> torrents_;
MockMediator mediator_{ *this };
static auto constexpr TorFilenames = std::array{
"Android-x86 8.1 r6 iso.torrent"sv,
"debian-11.2.0-amd64-DVD-1.iso.torrent"sv,
"ubuntu-18.04.6-desktop-amd64.iso.torrent"sv,
"ubuntu-20.04.4-desktop-amd64.iso.torrent"sv,
};
};
TEST_F(TorrentQueueTest, addRemoveToFromQueue)
{
auto queue = tr_torrent_queue{ mediator_ };
auto owned = std::vector<std::unique_ptr<tr_torrent>>{};
for (auto const& name : TorFilenames)
{
auto const path = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, '/', name };
auto tm = tr_torrent_metainfo{};
EXPECT_TRUE(tm.parse_torrent_file(path));
auto& tor = owned.emplace_back(std::make_unique<tr_torrent>(std::move(tm)));
tor->init_id(std::size(owned));
torrents_.try_emplace(tor->id(), *tor);
queue.add(tor->id());
}
for (size_t i = 0; i < std::size(owned); ++i)
{
EXPECT_EQ(i, queue.get_pos(owned[i]->id()));
}
queue.remove(owned[1]->id());
queue.remove(owned[2]->id());
owned.erase(std::begin(owned) + 1, std::begin(owned) + 3);
for (size_t i = 0; i < std::size(owned); ++i)
{
EXPECT_EQ(i, queue.get_pos(owned[i]->id()));
}
}
TEST_F(TorrentQueueTest, setQueuePos)
{
static auto constexpr QueuePos = std::array{ 1U, 3U, 0U, 2U };
auto queue = tr_torrent_queue{ mediator_ };
auto owned = std::vector<std::unique_ptr<tr_torrent>>{};
for (auto const& name : TorFilenames)
{
auto const path = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, '/', name };
auto tm = tr_torrent_metainfo{};
EXPECT_TRUE(tm.parse_torrent_file(path));
auto& tor = owned.emplace_back(std::make_unique<tr_torrent>(std::move(tm)));
tor->init_id(std::size(owned));
torrents_.try_emplace(tor->id(), *tor);
queue.add(tor->id());
}
for (size_t i = 0; i < std::size(owned); ++i)
{
EXPECT_EQ(i, queue.get_pos(owned[i]->id()));
}
for (size_t i = 0; i < std::size(owned); ++i)
{
auto const id = owned[i]->id();
auto const pos = QueuePos[i];
queue.set_pos(id, pos);
EXPECT_EQ(queue.get_pos(id), pos);
}
for (size_t i = 0; i < std::size(owned); ++i)
{
EXPECT_EQ(queue.get_pos(owned[i]->id()), QueuePos[i]);
}
}
TEST_F(TorrentQueueTest, toFromFile)
{
static auto constexpr ExpectedContents =
"[\n"
" \"70341e8e1fe8778af23f6318ca75a22f8b1f1c05.torrent\",\n"
" \"c9a337562cb0360fd6f5ab40fd2b1b81d5325dbd.torrent\",\n"
" \"bc26c6bc83d0ca1a7bf9875df1ffc3fed81ff555.torrent\",\n"
" \"f09c8d0884590088f4004e010a928f8b6178c2fd.torrent\"\n"
"]"sv;
auto queue = tr_torrent_queue{ mediator_ };
auto owned = std::vector<std::unique_ptr<tr_torrent>>{};
for (auto const& name : TorFilenames)
{
auto const path = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, '/', name };
auto tm = tr_torrent_metainfo{};
EXPECT_TRUE(tm.parse_torrent_file(path));
auto& tor = owned.emplace_back(std::make_unique<tr_torrent>(std::move(tm)));
tor->init_id(std::size(owned));
torrents_.try_emplace(tor->id(), *tor);
queue.add(tor->id());
}
queue.to_file();
auto f = std::ifstream{ sandboxDir() + "/queue.json" };
auto const contents = std::string{ std::istreambuf_iterator{ f }, std::istreambuf_iterator<decltype(f)::char_type>{} };
EXPECT_EQ(contents, ExpectedContents);
f.close();
auto const filenames = queue.from_file();
ASSERT_EQ(std::size(filenames), std::size(owned));
for (size_t i = 0; i < std::size(filenames); ++i)
{
EXPECT_EQ(filenames[i], owned[i]->store_filename());
}
}