diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj index 709b14a8c..8f4848f8e 100644 --- a/Transmission.xcodeproj/project.pbxproj +++ b/Transmission.xcodeproj/project.pbxproj @@ -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 = ""; }; ED9862952B979AA2002F3035 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Utils.h; sourceTree = ""; }; ED9862962B979AA2002F3035 /* Utils.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = Utils.mm; sourceTree = ""; }; + EDBA61FD2D4180D5001470F8 /* torrent-queue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "torrent-queue.h"; sourceTree = ""; }; + EDBA61FE2D4180D5001470F8 /* torrent-queue.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "torrent-queue.cc"; sourceTree = ""; }; EDBAAC8B29E486BC00D9495F /* ip-cache.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; fileEncoding = 4; path = "ip-cache.h"; sourceTree = ""; }; EDBAAC8D29E486C200D9495F /* ip-cache.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "ip-cache.cc"; sourceTree = ""; }; EDBDFA9D25AFCCA60093D9C1 /* evutil_time.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = evutil_time.c; sourceTree = ""; }; @@ -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 */, diff --git a/libtransmission/CMakeLists.txt b/libtransmission/CMakeLists.txt index 468c12c55..e4b529d47 100644 --- a/libtransmission/CMakeLists.txt +++ b/libtransmission/CMakeLists.txt @@ -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 diff --git a/libtransmission/quark.cc b/libtransmission/quark.cc index 6e816e5b1..3f096f349 100644 --- a/libtransmission/quark.cc +++ b/libtransmission/quark.cc @@ -170,7 +170,6 @@ auto constexpr MyStatic = std::array{ "isStalled"sv, "isUTP"sv, "isUploadingTo"sv, - "is_queued"sv, "labels"sv, "lastAnnouncePeerCount"sv, "lastAnnounceResult"sv, diff --git a/libtransmission/quark.h b/libtransmission/quark.h index a90f1fddb..9c4c5938d 100644 --- a/libtransmission/quark.h +++ b/libtransmission/quark.h @@ -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, diff --git a/libtransmission/resume.cc b/libtransmission/resume.cc index ce020255b..e6a14d325 100644 --- a/libtransmission/resume.cc +++ b/libtransmission/resume.cc @@ -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(); - 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(); - if (map == nullptr) - { - return ret; - } - - if (auto val = map->value_if(TR_KEY_queuePosition)) - { - helper.load_queue_position(*val); - ret = tr_resume::QueueState; - } - - if (auto val = map->value_if(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())) diff --git a/libtransmission/resume.h b/libtransmission/resume.h index 800ae2aff..c9449bdb6 100644 --- a/libtransmission/resume.h +++ b/libtransmission/resume.h @@ -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 }; diff --git a/libtransmission/session.cc b/libtransmission/session.cc index a386b5afc..4289b1bfc 100644 --- a/libtransmission/session.cc +++ b/libtransmission/session.cc @@ -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* 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& queue_order) +{ + auto files = tr_sys_dir_get_files(folder); + auto ret = std::vector{}; + 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* 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{}](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{}; - 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); } diff --git a/libtransmission/session.h b/libtransmission/session.h index d59b55997..aa4e7aaf0 100644 --- a/libtransmission/session.h +++ b/libtransmission/session.h @@ -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 diff --git a/libtransmission/torrent-metainfo.cc b/libtransmission/torrent-metainfo.cc index 8310ebec3..f778f769f 100644 --- a/libtransmission/torrent-metainfo.cc +++ b/libtransmission/torrent-metainfo.cc @@ -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); diff --git a/libtransmission/torrent-metainfo.h b/libtransmission/torrent-metainfo.h index d9c9718cc..7f14d236d 100644 --- a/libtransmission/torrent-metainfo.h +++ b/libtransmission/torrent-metainfo.h @@ -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"); } diff --git a/libtransmission/torrent-queue.cc b/libtransmission/torrent-queue.cc new file mode 100644 index 000000000..1770188bb --- /dev/null +++ b/libtransmission/torrent-queue.cc @@ -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 +#include +#include +#include + +#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(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(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 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(); + if (vec == nullptr) + { + return {}; + } + + auto ret = std::vector{}; + ret.reserve(std::size(*vec)); + for (auto const& var : *vec) + { + if (auto file = var.value_if(); file) + { + ret.emplace_back(*file); + } + } + + return ret; +} diff --git a/libtransmission/torrent-queue.h b/libtransmission/torrent-queue.h new file mode 100644 index 000000000..a9273c8e4 --- /dev/null +++ b/libtransmission/torrent-queue.h @@ -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 +#include +#include +#include + +#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 from_file(); + + static auto constexpr MinQueuePosition = size_t{}; + static auto constexpr MaxQueuePosition = ~size_t{}; + +private: + std::vector queue_; + std::vector pos_cache_; + + Mediator const& mediator_; +}; diff --git a/libtransmission/torrent.cc b/libtransmission/torrent.cc index beb6f7a89..cdf635fb9 100644 --- a/libtransmission/torrent.cc +++ b/libtransmission/torrent.cc @@ -6,10 +6,8 @@ #include #include #include // EINVAL -#include /* INT_MAX */ #include // size_t #include -#include #include #include #include @@ -464,132 +462,59 @@ void tr_torrent::stop_if_seed_limit_reached() // --- Queue -namespace -{ -namespace queue_helpers -{ -constexpr auto MinQueuePosition = std::numeric_limits::min(); -constexpr auto MaxQueuePosition = std::numeric_limits::max(); - -#ifdef TR_ENABLE_ASSERTS -[[nodiscard]] bool torrents_are_sorted_by_queue_position(std::vector 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(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(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(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(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; diff --git a/libtransmission/torrent.h b/libtransmission/torrent.h index b2dc7944b..d706312c8 100644 --- a/libtransmission/torrent.h +++ b/libtransmission/torrent.h @@ -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; diff --git a/tests/libtransmission/CMakeLists.txt b/tests/libtransmission/CMakeLists.txt index 4c435c20f..76ba4950a 100644 --- a/tests/libtransmission/CMakeLists.txt +++ b/tests/libtransmission/CMakeLists.txt @@ -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 diff --git a/tests/libtransmission/torrent-queue-test.cc b/tests/libtransmission/torrent-queue-test.cc new file mode 100644 index 000000000..914021e8e --- /dev/null +++ b/tests/libtransmission/torrent-queue-test.cc @@ -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 +#include +#include +#include +#include + +#include +#include + +#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 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>{}; + 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(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>{}; + 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(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>{}; + 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(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{} }; + 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()); + } +}