Files
transmission-mirror/gtk/Session.cc
Yat Ho 05aef3e787 refactor: unify quarks and strings to snake_case (#7108)
* refactor: change `leftUntilDone` to `left_until_done`

* refactor: change `magnetLink` to `magnet_link`

* refactor: change `manualAnnounceTime` to `manual_announce_time`

* refactor: change `maxConnectedPeers` to `max_connected_peers`

* refactor: change `metadataPercentComplete` to `metadata_percent_complete`

* refactor: change `peersConnected` to `peers_connected`

* refactor: change `peersFrom` to `peers_from`

* refactor: change `peersGettingFromUs` to `peers_getting_from_us`

* refactor: change `peersSendingToUs` to `peers_sending_to_us`

* refactor: change `percentComplete` to `percent_complete`

* refactor: change `percentDone` to `percent_done`

* refactor: change `pieceCount` to `piece_count`

* refactor: use quark when possible

* refactor: change `pieceSize` to `piece_size`

* refactor: change `primary-mime-type` to `primary_mime_type`

* refactor: change `rateDownload` to `rate_download`

* refactor: change `rateUpload` to `rate_upload`

* refactor: change `recheckProgress` to `recheck_progress`

* refactor: change `secondsDownloading` to `seconds_downloading`

* refactor: change `secondsSeeding` to `seconds_seeding`

* refactor: change `sizeWhenDone` to `size_when_done`

* refactor: change `startDate` to `start_date`

* refactor: change `trackerStats` to `tracker_stats`

* refactor: change `totalSize` to `total_size`

* refactor: change `torrentFile` to `torrent_file`

* refactor: change `uploadedEver` to `uploaded_ever`

* refactor: change `uploadRatio` to `upload_ratio`

* refactor: change `webseedsSendingToUs` to `webseeds_sending_to_us`

* refactor: change `bytesCompleted` to `bytes_completed`

* refactor: change `clientName` to `client_name`

* refactor: change `clientIsChoked` to `client_is_choked`

* refactor: change `clientIsInterested` to `client_is_interested`

* refactor: change `flagStr` to `flag_str`

* refactor: change `isDownloadingFrom` to `is_downloading_from`

* refactor: change `isEncrypted` to `is_encrypted`

* refactor: change `isIncoming` to `is_incoming`

* refactor: change `isUploadingTo` to `is_uploading_to`

* refactor: change `isUTP` to `is_utp`

* refactor: change `peerIsChoked` to `peer_is_choked`

* refactor: change `peerIsInterested` to `peer_is_interested`

* refactor: change `rateToClient` to `rate_to_client`

* refactor: change `rateToPeer` to `rate_to_peer`

* refactor: change `fromCache` to `from_cache`

* refactor: change `fromDht` to `from_dht`

* refactor: change `fromIncoming` to `from_incoming`

* refactor: change `fromLpd` to `from_lpd`

* refactor: change `fromLtep` to `from_ltep`

* refactor: change `fromPex` to `from_pex`

* refactor: change `fromTracker` to `from_tracker`

* refactor: change `announceState` to `announce_state`

* refactor: change `downloadCount` to `download_count`

* refactor: change `hasAnnounced` to `has_announced`

* refactor: change `hasScraped` to `has_scraped`

* refactor: change `isBackup` to `is_backup`

* refactor: change `lastAnnouncePeerCount` to `last_announce_peer_count`

* refactor: change `lastAnnounceResult` to `last_announce_result`

* refactor: change `lastAnnounceStartTime` to `last_announce_start_time`

* refactor: change `lastAnnounceSucceeded` to `last_announce_succeeded`

* refactor: change `lastAnnounceTime` to `last_announce_time`

* refactor: change `lastAnnounceTimedOut` to `last_announce_timed_out`

* refactor: change `lastScrapeResult` to `last_scrape_result`

* refactor: change `lastScrapeStartTime` to `last_scrape_start_time`

* refactor: change `lastScrapeSucceeded` to `last_scrape_succeeded`

* refactor: change `lastScrapeTime` to `last_scrape_time`

* refactor: change `lastScrapeTimedOut` to `last_scrape_timed_out`

* refactor: change `leecherCount` to `leecher_count`

* refactor: change `nextAnnounceTime` to `next_announce_time`

* refactor: change `nextScrapeTime` to `next_scrape_time`

* refactor: change `scrapeState` to `scrape_state`

* refactor: change `seederCount` to `seeder_count`

* refactor: change `torrent-added` to `torrent_added`

* refactor: change `torrent-duplicate` to `torrent_duplicate`

* refactor: change `torrent-remove` to `torrent_remove`

* refactor: change `delete-local-data` to `delete_local_data`

* refactor: change `torrent-rename-path` to `torrent_rename_path`

* refactor: change `alt-speed-down` to `alt_speed_down`

* refactor: convert `pref_toggle_entries` to quark array

* refactor: change `alt-speed-enabled` to `alt_speed_enabled`

* refactor: change `compact-view` to `compact_view`

* refactor: change `sort-reversed` to `sort_reversed`

* refactor: change `show-filterbar` to `show_filterbar`

* refactor: change `show-statusbar` to `show_statusbar`

* refactor: change `show-toolbar` to `show_toolbar`

* refactor: change `alt-speed-time-begin` to `alt_speed_time_begin`

* refactor: change `alt-speed-time-day` to `alt_speed_time_day`

* refactor: change `alt-speed-time-end` to `alt_speed_time_end`

* refactor: change `alt-speed-up` to `alt_speed_up`

* refactor: change `alt-speed-time-enabled` to `alt_speed_time_enabled`

* refactor: change `blocklist-enabled` to `blocklist_enabled`

* refactor: change `blocklist-size` to `blocklist_size`

* refactor: change `blocklist-url` to `blocklist_url`

* refactor: change `cache-size-mb` to `cache_size_mb`

* refactor: change `config-dir` to `config_dir`

* refactor: change `default-trackers` to `default_trackers`

* refactor: change `dht-enabled` to `dht_enabled`

* refactor: change `download-dir-free-space` to `download_dir_free_space`

* refactor: change `download-queue-enabled` to `download_queue_enabled`

* refactor: change `download-queue-size` to `download_queue_size`

* refactor: change `idle-seeding-limit-enabled` to `idle_seeding_limit_enabled`

* refactor: change `idle-seeding-limit` to `idle_seeding_limit`

* refactor: change `incomplete-dir-enabled` to `incomplete_dir_enabled`

* refactor: change `incomplete-dir` to `incomplete_dir`

* refactor: change `lpd-enabled` to `lpd_enabled`

* refactor: change `peer-limit-global` to `peer_limit_global`

* refactor: change `peer-limit-per-torrent` to `peer_limit_per_torrent`

* refactor: change `peer-port-random-on-start` to `peer_port_random_on_start`

* refactor: change `peer-port` to `peer_port`

* refactor: change `pex-enabled` to `pex_enabled`

* refactor: change `port-forwarding-enabled` to `port_forwarding_enabled`

* refactor: change `queue-stalled-enabled` to `queue_stalled_enabled`

* refactor: change `queue-stalled-minutes` to `queue_stalled_minutes`

* refactor: change `rename-partial-files` to `rename_partial_files`

* refactor: change `rpc-version-minimum` to `rpc_version_minimum`

* refactor: change `rpc-version-semver` to `rpc_version_semver`

* refactor: change `rpc-version` to `rpc_version`

* refactor: change `script-torrent-added-enabled` to `script_torrent_added_enabled`

* refactor: change `script-torrent-added-filename` to `script_torrent_added_filename`

* refactor: change `script-torrent-done-enabled` to `script_torrent_done_enabled`

* refactor: change `script-torrent-done-filename` to `script_torrent_done_filename`

* refactor: change `script-torrent-done-seeding-enabled` to `script_torrent_done_seeding_enabled`

* refactor: change `script-torrent-done-seeding-filename` to `script_torrent_done_seeding_filename`

* refactor: change `seed-queue-enabled` to `seed_queue_enabled`

* refactor: change `seed-queue-size` to `seed_queue_size`

* refactor: change `seedRatioLimited` to `seed_ratio_limited`

* refactor: change `session-id` to `session_id`

* refactor: change `speed-limit-down-enabled` to `speed_limit_down_enabled`

* refactor: change `speed-limit-down` to `speed_limit_down`

* refactor: change `speed-limit-up-enabled` to `speed_limit_up_enabled`

* refactor: change `speed-limit-up` to `speed_limit_up`

* refactor: change `start-added-torrents` to `start_added_torrents`

* refactor: change `trash-original-torrent-files` to `trash_original_torrent_files`

* refactor: change `utp-enabled` to `utp_enabled`

* refactor: change `tcp-enabled` to `tcp_enabled`

* docs: add missing docs for RPC `tcp_enabled`

* refactor: change `speed-units` to `speed_units`

* refactor: change `speed-bytes` to `speed_bytes`

* refactor: change `size-units` to `size_units`

* refactor: change `size-bytes` to `size_bytes`

* refactor: change `memory-units` to `memory_units`

* refactor: change `memory-bytes` to `memory_bytes`

* refactor: change `session-set` to `session_set`

* refactor: change `session-get` to `session_get`

* refactor: change `session-stats` to `session_stats`

* refactor: change `activeTorrentCount` to `active_torrent_count`

* refactor: change `downloadSpeed` to `download_speed`

* refactor: change `pausedTorrentCount` to `paused_torrent_count`

* refactor: change `torrentCount` to `torrent_count`

* refactor: change `uploadSpeed` to `upload_speed`

* refactor: change `cumulative-stats` to `cumulative_stats`

* refactor: change `current-stats` to `current_stats`

* refactor: change `uploadedBytes` and `uploaded-bytes` to `uploaded_bytes`

* refactor: change `downloadedBytes` and `downloaded-bytes` to `downloaded_bytes`

* refactor: change `filesAdded` and `files-added` to `files_added`

* refactor: change `sessionCount` and `session-count` to `session_count`

* refactor: change `secondsActive` and `seconds-active` to `seconds_active`

* refactor: change `blocklist-update` to `blocklist_update`

* refactor: change `port-test` to `port_test`

* refactor: change `session-close` to `session_close`

* refactor: change `queue-move-top` to `queue_move_top`

* refactor: change `queue-move-up` to `queue_move_up`

* refactor: change `queue-move-down` to `queue_move_down`

* refactor: change `queue-move-bottom` to `queue_move_bottom`

* refactor: change `free-space` to `free_space`

* refactor: change `group-set` to `group_set`

* refactor: change `group-get` to `group_get`

* refactor: change `announce-ip` to `announce_ip`

* refactor: change `announce-ip-enabled` to `announce_ip_enabled`

* refactor: change `upload-slots-per-torrent` to `upload_slots_per_torrent`

* refactor: change `trash-can-enabled` to `trash_can_enabled`

* refactor: change `watch-dir-enabled` to `watch_dir_enabled`

* refactor: change `watch-dir-force-generic` to `watch_dir_force_generic`

* refactor: change `watch-dir` to `watch_dir`

* refactor: change `message-level` to `message_level`

* refactor: change `scrape-paused-torrents-enabled` to `scrape_paused_torrents_enabled`

* refactor: change `torrent-added-verify-mode` to `torrent_added_verify_mode`

* refactor: change `sleep-per-seconds-during-verify` to `sleep_per_seconds_during_verify`

* refactor: change `bind-address-ipv4` to `bind_address_ipv4`

* refactor: change `bind-address-ipv6` to `bind_address_ipv6`

* refactor: change `peer-congestion-algorithm` to `peer_congestion_algorithm`

* refactor: change `peer-socket-tos` to `peer_socket_tos`

* refactor: change `peer-port-random-high` to `peer_port_random_high`

* refactor: change `peer-port-random-low` to `peer_port_random_low`

* refactor: change `anti-brute-force-enabled` to `anti_brute_force_enabled`

* refactor: change `rpc-authentication-required` to `rpc_authentication_required`

* refactor: change `rpc-bind-address` to `rpc_bind_address`

* refactor: change `rpc-enabled` to `rpc_enabled`

* refactor: change `rpc-host-whitelist` to `rpc_host_whitelist`

* refactor: change `rpc-host-whitelist-enabled` to `rpc_host_whitelist_enabled`

* refactor: change `rpc-password` to `rpc_password`

* refactor: change `rpc-port` to `rpc_port`

* refactor: change `rpc-socket-mode` to `rpc_socket_mode`

* refactor: change `rpc-url` to `rpc_url`

* refactor: change `rpc-username` to `rpc_username`

* refactor: change `rpc-whitelist` to `rpc_whitelist`

* refactor: change `rpc-whitelist-enabled` to `rpc_whitelist_enabled`

* refactor: change `ratio-limit-enabled` to `ratio_limit_enabled`

* refactor: change `ratio-limit` to `ratio_limit`

* refactor: change `show-options-window` to `show_options_window`

* refactor: change `open-dialog-dir` to `open_dialog_dir`

* refactor: change `inhibit-desktop-hibernation` to `inhibit_desktop_hibernation`

* refactor: change `show-notification-area-icon` to `show_notification_area_icon`

* refactor: change `start-minimized` to `start_minimized`

* refactor: change `torrent-added-notification-enabled` to `torrent_added_notification_enabled`

* refactor: change `anti-brute-force-threshold` to `anti_brute_force_threshold`

* refactor: change `torrent-complete-notification-enabled` to `torrent_complete_notification_enabled`

* refactor: change `prompt-before-exit` to `prompt_before_exit`

* refactor: change `sort-mode` to `sort_mode`

* refactor: change `statusbar-stats` to `statusbar_stats`

* refactor: change `show-extra-peer-details` to `show_extra_peer_details`

* refactor: change `show-backup-trackers` to `show_backup_trackers`

* refactor: change `blocklist-date` to `blocklist_date`

* refactor: change `blocklist-updates-enabled` to `blocklist_updates_enabled`

* refactor: change `main-window-layout-order` to `main_window_layout_order`

* refactor: change `main-window-height` to `main_window_height`

* refactor: change `main-window-width` to `main_window_width`

* refactor: change `main-window-x` to `main_window_x`

* refactor: change `main-window-y` to `main_window_y`

* refactor: change `filter-mode` to `filter_mode`

* refactor: change `filter-trackers` to `filter_trackers`

* refactor: change `filter-text` to `filter_text`

* refactor: change `remote-session-enabled` to `remote_session_enabled`

* refactor: change `remote-session-host` to `remote_session_host`

* refactor: change `remote-session-https` to `remote_session_https`

* refactor: change `remote-session-password` to `remote_session_password`

* refactor: change `remote-session-port` to `remote_session_port`

* refactor: change `remote-session-requres-authentication` to `remote_session_requires_authentication`

* refactor: change `remote-session-username` to `remote_session_username`

* refactor: change `torrent-complete-sound-command` to `torrent_complete_sound_command`

* refactor: change `torrent-complete-sound-enabled` to `torrent_complete_sound_enabled`

* refactor: change `user-has-given-informed-consent` to `user_has_given_informed_consent`

* refactor: change `read-clipboard` to `read_clipboard`

* refactor: change `details-window-height` to `details_window_height`

* refactor: change `details-window-width` to `details_window_width`

* refactor: change `main-window-is-maximized` to `main_window_is_maximized`

* refactor: change `port-is-open` to `port_is_open`

* refactor: change `show-tracker-scrapes` to `show_tracker_scrapes`

* refactor: change `max-peers` to `max_peers`

* refactor: change `peers2-6` to `peers2_6`

* refactor: change `seeding-time-seconds` to `seeding_time_seconds`

* refactor: change `downloading-time-seconds` to `downloading_time_seconds`

* refactor: change `ratio-mode` to `ratio_mode`

* refactor: change `idle-limit` to `idle_limit`

* refactor: change `idle-mode` to `idle_mode`

* refactor: change `speed-Bps` to `speed_Bps`

* refactor: change `use-global-speed-limit` to `use_global_speed_limit`

* refactor: change `use-speed-limit` to `use_speed_limit`

* chore: remove TODO comment

* docs: add upgrade instructions to `5.0.0`

* chore: bump rpc semver major version

* chore: housekeeping
2025-12-01 16:08:18 -06:00

1483 lines
38 KiB
C++

// This file Copyright © Transmission authors and contributors.
// This file is licensed under the MIT (SPDX: MIT) license,
// A copy of this license can be found in licenses/ .
#include "Session.h"
#include "Actions.h"
#include "ListModelAdapter.h"
#include "Notify.h"
#include "Prefs.h"
#include "PrefsDialog.h"
#include "SortListModel.hh"
#include "Torrent.h"
#include "TorrentSorter.h"
#include "Utils.h"
#include <libtransmission/transmission.h>
#include <libtransmission/log.h>
#include <libtransmission/rpcimpl.h>
#include <libtransmission/torrent-metainfo.h>
#include <libtransmission/utils.h> // tr_time()
#include <libtransmission/variant.h>
#include <libtransmission/web-utils.h> // tr_urlIsValid()
#include <giomm/asyncresult.h>
#include <giomm/dbusconnection.h>
#include <giomm/fileinfo.h>
#include <giomm/filemonitor.h>
#include <giomm/liststore.h>
#include <glibmm/error.h>
#include <glibmm/fileutils.h>
#include <glibmm/i18n.h>
#include <glibmm/main.h>
#include <glibmm/miscutils.h>
#include <glibmm/stringutils.h>
#include <glibmm/variant.h>
#if GTKMM_CHECK_VERSION(4, 0, 0)
#include <gtkmm/sortlistmodel.h>
#else
#include <gtkmm/treemodelsort.h>
#endif
#include <fmt/format.h>
#include <algorithm>
#include <array>
#include <cinttypes> // PRId64
#include <cstring> // strstr
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
using namespace std::literals;
class Session::Impl
{
public:
Impl(Session& core, tr_session* session);
Impl& operator=(Impl&&) = delete;
Impl& operator=(Impl const&) = delete;
Impl(Impl&&) = delete;
Impl(Impl const&) = delete;
~Impl();
tr_session* close();
Glib::RefPtr<Gio::ListStore<Torrent>> get_raw_model() const;
Glib::RefPtr<SortListModel<Torrent>> get_model();
tr_session* get_session() const;
std::pair<Glib::RefPtr<Torrent>, guint> find_torrent_by_id(tr_torrent_id_t torrent_id) const;
size_t get_active_torrent_count() const;
bool get_port_test_pending(PortTestIpProtocol ip_protocol);
void set_port_test_pending(bool pending, PortTestIpProtocol ip_protocol);
void update();
void torrents_added();
void add_files(std::vector<Glib::RefPtr<Gio::File>> const& files, bool do_start, bool do_prompt, bool do_notify);
void add_ctor(tr_ctor* ctor, bool do_prompt, bool do_notify);
void add_torrent(Glib::RefPtr<Torrent> const& torrent, bool do_notify);
bool add_from_url(Glib::ustring const& url);
void remove_torrent(tr_torrent_id_t id, bool delete_files);
void send_rpc_request(tr_variant const& request, int64_t tag, std::function<void(tr_variant&)> const& response_func);
void commit_prefs_change(tr_quark key);
auto& signal_add_error()
{
return signal_add_error_;
}
auto& signal_add_prompt()
{
return signal_add_prompt_;
}
auto& signal_blocklist_updated()
{
return signal_blocklist_updated_;
}
auto& signal_busy()
{
return signal_busy_;
}
auto& signal_prefs_changed()
{
return signal_prefs_changed_;
}
auto& signal_port_tested()
{
return signal_port_tested_;
}
auto& signal_torrents_changed()
{
return signal_torrents_changed_;
}
[[nodiscard]] constexpr auto& favicon_cache()
{
return favicon_cache_;
}
private:
Glib::RefPtr<Session> get_core_ptr() const;
bool is_busy() const;
void add_to_busy(int addMe);
void inc_busy();
void dec_busy();
bool add_file(Glib::RefPtr<Gio::File> const& file, bool do_start, bool do_prompt, bool do_notify);
void add_file_async_callback(
Glib::RefPtr<Gio::File> const& file,
Glib::RefPtr<Gio::AsyncResult>& result,
tr_ctor* ctor,
bool do_prompt,
bool do_notify);
Glib::RefPtr<Torrent> create_new_torrent(tr_ctor* ctor);
void maybe_inhibit_hibernation();
void set_hibernation_allowed(bool allowed);
void watchdir_update();
void watchdir_scan();
void watchdir_monitor_file(Glib::RefPtr<Gio::File> const& file);
bool watchdir_idle();
void on_file_changed_in_watchdir(
Glib::RefPtr<Gio::File> const& file,
Glib::RefPtr<Gio::File> const& other_type,
IF_GLIBMM2_68(Gio::FileMonitor::Event, Gio::FileMonitorEvent) event_type);
void on_pref_changed(tr_quark key);
void on_torrent_completeness_changed(tr_torrent* tor, tr_completeness completeness, bool was_running);
void on_torrent_metadata_changed(tr_torrent* raw_torrent);
void on_torrent_removal_done(tr_torrent_id_t id, bool succeeded);
private:
Session& core_;
sigc::signal<void(ErrorCode, Glib::ustring const&)> signal_add_error_;
sigc::signal<void(tr_ctor*)> signal_add_prompt_;
sigc::signal<void(bool)> signal_blocklist_updated_;
sigc::signal<void(bool)> signal_busy_;
sigc::signal<void(tr_quark)> signal_prefs_changed_;
sigc::signal<void(std::optional<bool>, PortTestIpProtocol)> signal_port_tested_;
sigc::signal<void(std::unordered_set<tr_torrent_id_t> const&, Torrent::ChangeFlags)> signal_torrents_changed_;
Glib::RefPtr<Gio::FileMonitor> monitor_;
sigc::connection monitor_tag_;
Glib::RefPtr<Gio::File> monitor_dir_;
std::vector<Glib::RefPtr<Gio::File>> monitor_files_;
sigc::connection monitor_idle_tag_;
bool adding_from_watch_dir_ = false;
bool inhibit_allowed_ = false;
bool have_inhibit_cookie_ = false;
bool dbus_error_ = false;
std::array<bool, NUM_PORT_TEST_IP_PROTOCOL> port_test_pending_ = {};
guint inhibit_cookie_ = 0;
gint busy_count_ = 0;
Glib::RefPtr<Gio::ListStore<Torrent>> raw_model_;
Glib::RefPtr<SortListModel<Torrent>> sorted_model_;
Glib::RefPtr<TorrentSorter> sorter_ = TorrentSorter::create();
tr_session* session_ = nullptr;
FaviconCache<Glib::RefPtr<Gdk::Pixbuf>> favicon_cache_;
};
Glib::RefPtr<Session> Session::Impl::get_core_ptr() const
{
core_.reference();
return Glib::make_refptr_for_instance(&core_);
}
/***
****
***/
Glib::RefPtr<Gio::ListStore<Torrent>> Session::Impl::get_raw_model() const
{
return raw_model_;
}
Glib::RefPtr<Gio::ListModel> Session::get_model() const
{
return impl_->get_raw_model();
}
Glib::RefPtr<Session::Model> Session::get_sorted_model() const
{
return impl_->get_model();
}
Glib::RefPtr<SortListModel<Torrent>> Session::Impl::get_model()
{
return sorted_model_;
}
tr_session* Session::get_session() const
{
return impl_->get_session();
}
tr_session* Session::Impl::get_session() const
{
return session_;
}
/***
**** BUSY
***/
bool Session::Impl::is_busy() const
{
return busy_count_ > 0;
}
void Session::Impl::add_to_busy(int addMe)
{
bool const wasBusy = is_busy();
busy_count_ += addMe;
if (wasBusy != is_busy())
{
signal_busy_.emit(is_busy());
}
}
void Session::Impl::inc_busy()
{
add_to_busy(1);
}
void Session::Impl::dec_busy()
{
add_to_busy(-1);
}
/***
****
**** WATCHDIR
****
***/
namespace
{
time_t get_file_mtime(Glib::RefPtr<Gio::File> const& file)
{
try
{
return file->query_info(G_FILE_ATTRIBUTE_TIME_MODIFIED)->get_attribute_uint64(G_FILE_ATTRIBUTE_TIME_MODIFIED);
}
catch (Glib::Error const&)
{
return 0;
}
}
void rename_torrent(Glib::RefPtr<Gio::File> const& file)
{
auto info = Glib::RefPtr<Gio::FileInfo>();
try
{
info = file->query_info(G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME);
}
catch (Glib::Error const&)
{
return;
}
auto const old_name = info->get_attribute_as_string(G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME);
auto const new_name = fmt::format("{}.added", old_name);
try
{
file->set_display_name(new_name);
}
catch (Glib::Error const& e)
{
gtr_message(fmt::format(
fmt::runtime(_("Couldn't rename '{old_path}' as '{path}': {error} ({error_code})")),
fmt::arg("old_path", old_name),
fmt::arg("path", new_name),
fmt::arg("error", e.what()),
fmt::arg("error_code", e.code())));
}
}
} // namespace
bool Session::Impl::watchdir_idle()
{
std::vector<Glib::RefPtr<Gio::File>> changing;
std::vector<Glib::RefPtr<Gio::File>> unchanging;
time_t const now = tr_time();
/* separate the files into two lists: changing and unchanging */
for (auto const& file : monitor_files_)
{
time_t const mtime = get_file_mtime(file);
if (mtime + 2 >= now)
{
changing.push_back(file);
}
else
{
unchanging.push_back(file);
}
}
/* add the files that have stopped changing */
if (!unchanging.empty())
{
bool const do_start = gtr_pref_flag_get(TR_KEY_start_added_torrents);
bool const do_prompt = gtr_pref_flag_get(TR_KEY_show_options_window);
adding_from_watch_dir_ = true;
add_files(unchanging, do_start, do_prompt, true);
std::for_each(unchanging.begin(), unchanging.end(), rename_torrent);
adding_from_watch_dir_ = false;
}
/* keep monitoring the ones that are still changing */
monitor_files_ = changing;
/* if monitor_files is nonempty, keep checking every second */
if (!monitor_files_.empty())
{
return true;
}
monitor_idle_tag_.disconnect();
return false;
}
/* If this file is a torrent, add it to our list */
void Session::Impl::watchdir_monitor_file(Glib::RefPtr<Gio::File> const& file)
{
auto const filename = file->get_path();
bool const is_torrent = Glib::str_has_suffix(filename, ".torrent");
if (is_torrent)
{
/* if we're not already watching this file, start watching it now */
bool const found = std::any_of(
monitor_files_.begin(),
monitor_files_.end(),
[file](auto const& f) { return file->equal(f); });
if (!found)
{
monitor_files_.push_back(file);
if (!monitor_idle_tag_.connected())
{
monitor_idle_tag_ = Glib::signal_timeout().connect_seconds(sigc::mem_fun(*this, &Impl::watchdir_idle), 1);
}
}
}
}
/* GFileMonitor noticed a file was created */
void Session::Impl::on_file_changed_in_watchdir(
Glib::RefPtr<Gio::File> const& file,
Glib::RefPtr<Gio::File> const& /*other_type*/,
IF_GLIBMM2_68(Gio::FileMonitor::Event, Gio::FileMonitorEvent) event_type)
{
if (event_type == TR_GIO_FILE_MONITOR_EVENT(CREATED))
{
watchdir_monitor_file(file);
}
}
/* walk through the pre-existing files in the watchdir */
void Session::Impl::watchdir_scan()
{
auto const dirname = gtr_pref_string_get(TR_KEY_watch_dir);
try
{
for (auto const& name : Glib::Dir(dirname))
{
watchdir_monitor_file(Gio::File::create_for_path(Glib::build_filename(dirname, name)));
}
}
catch (Glib::FileError const&)
{
}
}
void Session::Impl::watchdir_update()
{
bool const is_enabled = gtr_pref_flag_get(TR_KEY_watch_dir_enabled);
auto const dir = Gio::File::create_for_path(gtr_pref_string_get(TR_KEY_watch_dir));
if (monitor_ != nullptr && (!is_enabled || !dir->equal(monitor_dir_)))
{
monitor_tag_.disconnect();
monitor_->cancel();
monitor_dir_.reset();
monitor_.reset();
}
if (!is_enabled || monitor_ != nullptr)
{
return;
}
auto monitor = Glib::RefPtr<Gio::FileMonitor>();
try
{
monitor = dir->monitor_directory();
}
catch (Glib::Error const&)
{
return;
}
watchdir_scan();
monitor_ = monitor;
monitor_dir_ = dir;
monitor_tag_ = monitor_->signal_changed().connect(sigc::mem_fun(*this, &Impl::on_file_changed_in_watchdir));
}
/***
****
***/
void Session::Impl::on_pref_changed(tr_quark const key)
{
switch (key)
{
case TR_KEY_sort_mode:
sorter_->set_mode(gtr_pref_string_get(TR_KEY_sort_mode));
break;
case TR_KEY_sort_reversed:
sorter_->set_reversed(gtr_pref_flag_get(TR_KEY_sort_reversed));
break;
case TR_KEY_peer_limit_global:
tr_sessionSetPeerLimit(session_, gtr_pref_int_get(key));
break;
case TR_KEY_peer_limit_per_torrent:
tr_sessionSetPeerLimitPerTorrent(session_, gtr_pref_int_get(key));
break;
case TR_KEY_inhibit_desktop_hibernation:
maybe_inhibit_hibernation();
break;
case TR_KEY_watch_dir:
case TR_KEY_watch_dir_enabled:
watchdir_update();
break;
default:
break;
}
}
/**
***
**/
Glib::RefPtr<Session> Session::create(tr_session* session)
{
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
return Glib::make_refptr_for_instance(new Session(session));
}
Session::Session(tr_session* session)
: Glib::ObjectBase(typeid(Session))
, impl_(std::make_unique<Impl>(*this, session))
{
}
Session::~Session() = default;
Session::Impl::Impl(Session& core, tr_session* session)
: core_{ core }
, session_{ session }
{
raw_model_ = Gio::ListStore<Torrent>::create();
signal_torrents_changed_.connect(sigc::hide<0>(sigc::mem_fun(*sorter_, &TorrentSorter::update)));
sorted_model_ = SortListModel<Torrent>::create(gtr_ptr_static_cast<Gio::ListModel>(raw_model_), sorter_);
/* init from prefs & listen to pref changes */
on_pref_changed(TR_KEY_sort_mode);
on_pref_changed(TR_KEY_sort_reversed);
on_pref_changed(TR_KEY_watch_dir_enabled);
on_pref_changed(TR_KEY_peer_limit_global);
on_pref_changed(TR_KEY_inhibit_desktop_hibernation);
signal_prefs_changed_.connect([this](auto key) { on_pref_changed(key); });
tr_sessionSetMetadataCallback(
session,
[](auto* /*session*/, auto* tor, gpointer impl) { static_cast<Impl*>(impl)->on_torrent_metadata_changed(tor); },
this);
tr_sessionSetCompletenessCallback(
session,
[](auto* tor, auto completeness, bool was_running, gpointer impl)
{ static_cast<Impl*>(impl)->on_torrent_completeness_changed(tor, completeness, was_running); },
this);
}
Session::Impl::~Impl()
{
monitor_idle_tag_.disconnect();
}
tr_session* Session::close()
{
return impl_->close();
}
tr_session* Session::Impl::close()
{
auto* session = session_;
if (session != nullptr)
{
session_ = nullptr;
gtr_pref_save(session);
}
return session;
}
/***
**** COMPLETENESS CALLBACK
***/
/* this is called in the libtransmission thread, *NOT* the GTK+ thread,
so delegate to the GTK+ thread before calling notify's dbus code... */
void Session::Impl::on_torrent_completeness_changed(tr_torrent* tor, tr_completeness completeness, bool was_running)
{
if (was_running && completeness != TR_LEECH && tr_torrentStat(tor)->sizeWhenDone != 0)
{
Glib::signal_idle().connect(
[core = get_core_ptr(), torrent_id = tr_torrentId(tor)]()
{
gtr_notify_torrent_completed(core, torrent_id);
return false;
});
}
}
/***
**** METADATA CALLBACK
***/
namespace
{
struct metadata_callback_data
{
Session* core;
tr_torrent_id_t torrent_id;
};
} // namespace
std::pair<Glib::RefPtr<Torrent>, guint> Session::Impl::find_torrent_by_id(tr_torrent_id_t torrent_id) const
{
auto begin_position = 0U;
auto end_position = raw_model_->get_n_items();
while (begin_position < end_position)
{
auto const position = begin_position + (end_position - begin_position) / 2;
auto const torrent = raw_model_->get_item(position);
auto const current_torrent_id = torrent->get_id();
if (current_torrent_id == torrent_id)
{
return { torrent, position };
}
if (current_torrent_id < torrent_id)
{
begin_position = position + 1;
}
else
{
end_position = position;
}
}
return {};
}
/* this is called in the libtransmission thread, *NOT* the GTK+ thread,
so delegate to the GTK+ thread before changing our list store... */
void Session::Impl::on_torrent_metadata_changed(tr_torrent* raw_torrent)
{
Glib::signal_idle().connect(
[this, core = get_core_ptr(), torrent_id = tr_torrentId(raw_torrent)]()
{
/* update the torrent's collated name */
if (auto const& [torrent, position] = find_torrent_by_id(torrent_id); torrent)
{
torrent->update();
}
return false;
});
}
/***
****
**** ADDING TORRENTS
****
***/
void Session::add_torrent(Glib::RefPtr<Torrent> const& torrent, bool do_notify)
{
impl_->add_torrent(torrent, do_notify);
}
void Session::Impl::add_torrent(Glib::RefPtr<Torrent> const& torrent, bool do_notify)
{
if (torrent != nullptr)
{
raw_model_->insert_sorted(torrent, &Torrent::compare_by_id);
if (do_notify)
{
gtr_notify_torrent_added(get_core_ptr(), torrent->get_id());
}
}
}
Glib::RefPtr<Torrent> Session::Impl::create_new_torrent(tr_ctor* ctor)
{
bool do_trash = false;
/* let the gtk client handle the removal, since libT
* doesn't have any concept of the glib trash API */
tr_ctorGetDeleteSource(ctor, &do_trash);
tr_ctorSetDeleteSource(ctor, false);
tr_torrent* const tor = tr_torrentNew(ctor, nullptr);
if (tor != nullptr && do_trash)
{
char const* config = tr_sessionGetConfigDir(session_);
char const* source = tr_ctorGetSourceFile(ctor);
if (source != nullptr)
{
/* #1294: don't delete the .torrent file if it's our internal copy */
bool const is_internal = strstr(source, config) == source;
if (!is_internal)
{
gtr_file_trash_or_remove(source, nullptr);
}
}
}
return Torrent::create(tor);
}
void Session::Impl::add_ctor(tr_ctor* ctor, bool do_prompt, bool do_notify)
{
auto const* metainfo = tr_ctorGetMetainfo(ctor);
if (metainfo == nullptr)
{
return;
}
if (tr_torrentFindFromMetainfo(get_session(), metainfo) != nullptr)
{
/* don't complain about torrent files in the watch directory
* that have already been added... that gets annoying and we
* don't want to be nagging users to clean up their watch dirs */
if (tr_ctorGetSourceFile(ctor) == nullptr || !adding_from_watch_dir_)
{
signal_add_error_.emit(ERR_ADD_TORRENT_DUP, metainfo->name().c_str());
}
tr_ctorFree(ctor);
return;
}
if (!do_prompt)
{
add_torrent(create_new_torrent(ctor), do_notify);
tr_ctorFree(ctor);
return;
}
signal_add_prompt_.emit(ctor);
}
namespace
{
void core_apply_defaults(tr_ctor* ctor)
{
if (!tr_ctorGetPaused(ctor, TR_FORCE, nullptr))
{
tr_ctorSetPaused(ctor, TR_FORCE, !gtr_pref_flag_get(TR_KEY_start_added_torrents));
}
if (!tr_ctorGetDeleteSource(ctor, nullptr))
{
tr_ctorSetDeleteSource(ctor, gtr_pref_flag_get(TR_KEY_trash_original_torrent_files));
}
if (!tr_ctorGetPeerLimit(ctor, TR_FORCE, nullptr))
{
tr_ctorSetPeerLimit(ctor, TR_FORCE, gtr_pref_int_get(TR_KEY_peer_limit_per_torrent));
}
if (!tr_ctorGetDownloadDir(ctor, TR_FORCE, nullptr))
{
tr_ctorSetDownloadDir(ctor, TR_FORCE, gtr_pref_string_get(TR_KEY_download_dir).c_str());
}
}
} // namespace
void Session::add_ctor(tr_ctor* ctor)
{
bool const do_notify = false;
bool const do_prompt = gtr_pref_flag_get(TR_KEY_show_options_window);
core_apply_defaults(ctor);
impl_->add_ctor(ctor, do_prompt, do_notify);
}
/***
****
***/
void Session::Impl::add_file_async_callback(
Glib::RefPtr<Gio::File> const& file,
Glib::RefPtr<Gio::AsyncResult>& result,
tr_ctor* ctor,
bool do_prompt,
bool do_notify)
{
try
{
gsize length = 0;
char* contents = nullptr;
if (!file->load_contents_finish(result, contents, length))
{
gtr_message(fmt::format(fmt::runtime(_("Couldn't read '{path}'")), fmt::arg("path", file->get_parse_name())));
}
else if (tr_ctorSetMetainfo(ctor, contents, length, nullptr))
{
add_ctor(ctor, do_prompt, do_notify);
}
else
{
tr_ctorFree(ctor);
}
}
catch (Glib::Error const& e)
{
gtr_message(fmt::format(
fmt::runtime(_("Couldn't read '{path}': {error} ({error_code})")),
fmt::arg("path", file->get_parse_name()),
fmt::arg("error", e.what()),
fmt::arg("error_code", e.code())));
}
dec_busy();
}
bool Session::Impl::add_file(Glib::RefPtr<Gio::File> const& file, bool do_start, bool do_prompt, bool do_notify)
{
auto* const session = get_session();
if (session == nullptr)
{
return false;
}
bool handled = false;
auto* ctor = tr_ctorNew(session);
core_apply_defaults(ctor);
tr_ctorSetPaused(ctor, TR_FORCE, !do_start);
bool loaded = false;
if (auto const path = file->get_path(); !std::empty(path))
{
// try to treat it as a file...
loaded = tr_ctorSetMetainfoFromFile(ctor, path.c_str(), nullptr);
}
if (!loaded)
{
// try to treat it as a magnet link...
loaded = tr_ctorSetMetainfoFromMagnetLink(ctor, file->get_uri().c_str(), nullptr);
}
// if we could make sense of it, add it
if (loaded)
{
handled = true;
add_ctor(ctor, do_prompt, do_notify);
}
else if (tr_urlIsValid(file->get_uri()))
{
handled = true;
inc_busy();
file->load_contents_async([this, file, ctor, do_prompt, do_notify](auto& result)
{ add_file_async_callback(file, result, ctor, do_prompt, do_notify); });
}
else
{
tr_ctorFree(ctor);
std::cerr << fmt::format(
fmt::runtime(_("Couldn't add torrent file '{path}'")),
fmt::arg("path", file->get_parse_name()))
<< '\n';
}
return handled;
}
bool Session::add_from_url(Glib::ustring const& url)
{
return impl_->add_from_url(url);
}
bool Session::Impl::add_from_url(Glib::ustring const& url)
{
auto const file = Gio::File::create_for_uri(url);
auto const do_start = gtr_pref_flag_get(TR_KEY_start_added_torrents);
auto const do_prompt = gtr_pref_flag_get(TR_KEY_show_options_window);
auto const do_notify = false;
auto const handled = add_file(file, do_start, do_prompt, do_notify);
torrents_added();
return handled;
}
void Session::add_files(std::vector<Glib::RefPtr<Gio::File>> const& files, bool do_start, bool do_prompt, bool do_notify)
{
impl_->add_files(files, do_start, do_prompt, do_notify);
}
void Session::Impl::add_files(std::vector<Glib::RefPtr<Gio::File>> const& files, bool do_start, bool do_prompt, bool do_notify)
{
for (auto const& file : files)
{
add_file(file, do_start, do_prompt, do_notify);
}
torrents_added();
}
void Session::torrents_added()
{
impl_->torrents_added();
}
void Session::Impl::torrents_added()
{
update();
signal_add_error_.emit(ERR_NO_MORE_TORRENTS, {});
}
void Session::torrent_changed(tr_torrent_id_t id)
{
if (auto const& [torrent, position] = impl_->find_torrent_by_id(id); torrent)
{
torrent->update();
}
}
void Session::remove_torrent(tr_torrent_id_t id, bool delete_files)
{
impl_->remove_torrent(id, delete_files);
}
void Session::Impl::remove_torrent(tr_torrent_id_t id, bool delete_files)
{
static auto const callback = [](tr_torrent_id_t processed_id, bool succeeded, void* user_data)
{
// "Own" the core since refcount has already been incremented before operation start — only decrement required.
auto const core = Glib::make_refptr_for_instance(static_cast<Session*>(user_data));
Glib::signal_idle().connect_once([processed_id, succeeded, core]()
{ core->impl_->on_torrent_removal_done(processed_id, succeeded); });
};
if (auto const& [torrent, position] = find_torrent_by_id(id); torrent)
{
// Extend core lifetime, refcount will be decremented in the callback.
core_.reference();
tr_torrentRemove(
&torrent->get_underlying(),
delete_files,
[](char const* filename, void* /*user_data*/, tr_error* error)
{ return gtr_file_trash_or_remove(filename, error); },
nullptr,
callback,
&core_);
}
}
void Session::Impl::on_torrent_removal_done(tr_torrent_id_t id, bool succeeded)
{
if (!succeeded)
{
return;
}
if (auto const& [torrent, position] = find_torrent_by_id(id); torrent)
{
get_raw_model()->remove(position);
}
}
void Session::load(bool force_paused)
{
auto* const ctor = tr_ctorNew(impl_->get_session());
if (force_paused)
{
tr_ctorSetPaused(ctor, TR_FORCE, true);
}
tr_ctorSetPeerLimit(ctor, TR_FALLBACK, gtr_pref_int_get(TR_KEY_peer_limit_per_torrent));
auto* session = impl_->get_session();
auto const n_torrents = tr_sessionLoadTorrents(session, ctor);
tr_ctorFree(ctor);
auto raw_torrents = std::vector<tr_torrent*>{};
raw_torrents.resize(n_torrents);
tr_sessionGetAllTorrents(session, std::data(raw_torrents), std::size(raw_torrents));
auto torrents = std::vector<Glib::RefPtr<Torrent>>();
torrents.reserve(raw_torrents.size());
std::transform(raw_torrents.begin(), raw_torrents.end(), std::back_inserter(torrents), &Torrent::create);
std::sort(torrents.begin(), torrents.end(), &Torrent::less_by_id);
auto const model = impl_->get_raw_model();
model->splice(0, model->get_n_items(), torrents);
}
void Session::clear()
{
impl_->get_raw_model()->remove_all();
}
/***
****
***/
void Session::update()
{
impl_->update();
}
void Session::start_now(tr_torrent_id_t id)
{
tr_variant top;
tr_variantInitDict(&top, 2);
tr_variantDictAddStrView(&top, TR_KEY_method, "torrent-start-now");
auto* args = tr_variantDictAddDict(&top, TR_KEY_arguments, 1);
auto* ids = tr_variantDictAddList(args, TR_KEY_ids, 1);
tr_variantListAddInt(ids, id);
exec(top);
}
void Session::Impl::update()
{
auto torrent_ids = std::unordered_set<tr_torrent_id_t>();
auto changes = Torrent::ChangeFlags();
/* update the model */
for (auto i = 0U, count = raw_model_->get_n_items(); i < count; ++i)
{
auto const torrent = raw_model_->get_item(i);
if (auto const torrent_changes = torrent->update(); torrent_changes.any())
{
torrent_ids.insert(torrent->get_id());
changes |= torrent_changes;
}
}
/* update hibernation */
maybe_inhibit_hibernation();
if (changes.any())
{
signal_torrents_changed_.emit(torrent_ids, changes);
}
}
/**
*** Hibernate
**/
namespace
{
auto const SessionManagerServiceName = "org.gnome.SessionManager"sv; // TODO(C++20): Use ""s
auto const SessionManagerInterface = "org.gnome.SessionManager"sv; // TODO(C++20): Use ""s
auto const SessionManagerObjectPath = "/org/gnome/SessionManager"sv; // TODO(C++20): Use ""s
bool gtr_inhibit_hibernation(guint32& cookie)
{
bool success = false;
char const* application = "Transmission BitTorrent Client";
char const* reason = "BitTorrent Activity";
int const toplevel_xid = 0;
int const flags = 4; /* Inhibit suspending the session or computer */
try
{
auto const connection = Gio::DBus::Connection::get_sync(TR_GIO_DBUS_BUS_TYPE(SESSION));
auto response = connection->call_sync(
std::string(SessionManagerObjectPath),
std::string(SessionManagerInterface),
"Inhibit",
Glib::VariantContainerBase::create_tuple({
Glib::Variant<Glib::ustring>::create(application),
Glib::Variant<guint32>::create(toplevel_xid),
Glib::Variant<Glib::ustring>::create(reason),
Glib::Variant<guint32>::create(flags),
}),
std::string(SessionManagerServiceName),
1000);
cookie = Glib::VariantBase::cast_dynamic<Glib::Variant<guint32>>(response.get_child(0)).get();
/* logging */
tr_logAddInfo(_("Inhibiting desktop hibernation"));
success = true;
}
catch (Glib::Error const& e)
{
tr_logAddError(
fmt::format(fmt::runtime(_("Couldn't inhibit desktop hibernation: {error}")), fmt::arg("error", e.what())));
}
return success;
}
void gtr_uninhibit_hibernation(guint inhibit_cookie)
{
try
{
auto const connection = Gio::DBus::Connection::get_sync(TR_GIO_DBUS_BUS_TYPE(SESSION));
connection->call_sync(
std::string(SessionManagerObjectPath),
std::string(SessionManagerInterface),
"Uninhibit",
Glib::VariantContainerBase::create_tuple({ Glib::Variant<guint32>::create(inhibit_cookie) }),
std::string(SessionManagerServiceName),
1000);
/* logging */
tr_logAddInfo(_("Allowing desktop hibernation"));
}
catch (Glib::Error const& e)
{
tr_logAddError(
fmt::format(fmt::runtime(_("Couldn't inhibit desktop hibernation: {error}")), fmt::arg("error", e.what())));
}
}
} // namespace
void Session::Impl::set_hibernation_allowed(bool allowed)
{
inhibit_allowed_ = allowed;
if (allowed && have_inhibit_cookie_)
{
gtr_uninhibit_hibernation(inhibit_cookie_);
have_inhibit_cookie_ = false;
}
if (!allowed && !have_inhibit_cookie_ && !dbus_error_)
{
if (gtr_inhibit_hibernation(inhibit_cookie_))
{
have_inhibit_cookie_ = true;
}
else
{
dbus_error_ = true;
}
}
}
void Session::Impl::maybe_inhibit_hibernation()
{
/* hibernation is allowed if EITHER
* (a) the "inhibit" pref is turned off OR
* (b) there aren't any active torrents */
bool const hibernation_allowed = !gtr_pref_flag_get(TR_KEY_inhibit_desktop_hibernation) || get_active_torrent_count() == 0;
set_hibernation_allowed(hibernation_allowed);
}
/**
*** Prefs
**/
void Session::Impl::commit_prefs_change(tr_quark const key)
{
signal_prefs_changed_.emit(key);
gtr_pref_save(session_);
}
void Session::set_pref(tr_quark const key, std::string const& newval)
{
if (newval != gtr_pref_string_get(key))
{
gtr_pref_string_set(key, newval);
impl_->commit_prefs_change(key);
}
}
void Session::set_pref(tr_quark const key, bool newval)
{
if (newval != gtr_pref_flag_get(key))
{
gtr_pref_flag_set(key, newval);
impl_->commit_prefs_change(key);
}
}
void Session::set_pref(tr_quark const key, int newval)
{
if (newval != gtr_pref_int_get(key))
{
gtr_pref_int_set(key, newval);
impl_->commit_prefs_change(key);
}
}
void Session::set_pref(tr_quark const key, double newval)
{
if (std::fabs(newval - gtr_pref_double_get(key)) >= 0.0001)
{
gtr_pref_double_set(key, newval);
impl_->commit_prefs_change(key);
}
}
/***
****
**** RPC Interface
****
***/
/* #define DEBUG_RPC */
namespace
{
int64_t nextTag = 1;
std::map<int64_t, std::function<void(tr_variant&)>> pendingRequests;
bool core_read_rpc_response_idle(tr_variant& response)
{
if (int64_t tag = 0; tr_variantDictFindInt(&response, TR_KEY_tag, &tag))
{
if (auto const data_it = pendingRequests.find(tag); data_it != pendingRequests.end())
{
if (auto const& response_func = data_it->second; response_func)
{
response_func(response);
}
pendingRequests.erase(data_it);
}
else
{
gtr_warning(fmt::format(fmt::runtime(_("Couldn't find pending RPC request for tag {tag}")), fmt::arg("tag", tag)));
}
}
return false;
}
void core_read_rpc_response(tr_session* /*session*/, tr_variant&& response)
{
auto owned_response = std::make_shared<tr_variant>(std::move(response));
Glib::signal_idle().connect([owned_response]() mutable { return core_read_rpc_response_idle(*owned_response); });
}
} // namespace
void Session::Impl::send_rpc_request(
tr_variant const& request,
int64_t tag,
std::function<void(tr_variant&)> const& response_func)
{
if (session_ == nullptr)
{
gtr_error("GTK+ client doesn't support connections to remote servers yet.");
}
else
{
/* remember this request */
pendingRequests.try_emplace(tag, response_func);
/* make the request */
#ifdef DEBUG_RPC
gtr_message(fmt::format("request: [{}]", tr_variantToStr(request, TR_VARIANT_FMT_JSON_LEAN)));
#endif
tr_rpc_request_exec(session_, request, core_read_rpc_response);
}
}
/***
**** Sending a test-port request via RPC
***/
void Session::port_test(PortTestIpProtocol const ip_protocol)
{
static auto constexpr IpStr = std::array{ "ipv4"sv, "ipv6"sv };
if (port_test_pending(ip_protocol))
{
return;
}
impl_->set_port_test_pending(true, ip_protocol);
auto const tag = nextTag++;
auto arguments_map = tr_variant::Map{ 1U };
arguments_map.try_emplace(TR_KEY_ip_protocol, tr_variant::unmanaged_string(IpStr[ip_protocol]));
auto request_map = tr_variant::Map{ 3U };
request_map.try_emplace(TR_KEY_method, tr_variant::unmanaged_string("port-test"sv));
request_map.try_emplace(TR_KEY_tag, tag);
request_map.try_emplace(TR_KEY_arguments, std::move(arguments_map));
impl_->send_rpc_request(
tr_variant{ std::move(request_map) },
tag,
[this, ip_protocol](tr_variant& response)
{
impl_->set_port_test_pending(false, ip_protocol);
auto status = std::optional<bool>{};
if (tr_variant* args = nullptr; tr_variantDictFindDict(&response, TR_KEY_arguments, &args))
{
if (auto result = bool{}; tr_variantDictFindBool(args, TR_KEY_port_is_open_kebab, &result))
{
status = result;
}
}
// If for whatever reason the status optional is empty here,
// then something must have gone wrong with the port test,
// so the UI should show the "error" state
impl_->signal_port_tested().emit(status, ip_protocol);
});
}
bool Session::port_test_pending(Session::PortTestIpProtocol ip_protocol) const noexcept
{
return impl_->get_port_test_pending(ip_protocol);
}
bool Session::Impl::get_port_test_pending(Session::PortTestIpProtocol ip_protocol)
{
return ip_protocol < NUM_PORT_TEST_IP_PROTOCOL && port_test_pending_[ip_protocol];
}
void Session::Impl::set_port_test_pending(bool pending, Session::PortTestIpProtocol ip_protocol)
{
if (ip_protocol < NUM_PORT_TEST_IP_PROTOCOL)
{
port_test_pending_[ip_protocol] = pending;
}
}
/***
**** Updating a blocklist via RPC
***/
void Session::blocklist_update()
{
auto const tag = nextTag;
++nextTag;
tr_variant request;
tr_variantInitDict(&request, 2);
tr_variantDictAddStrView(&request, TR_KEY_method, "blocklist-update");
tr_variantDictAddInt(&request, TR_KEY_tag, tag);
impl_->send_rpc_request(
request,
tag,
[this](auto& response)
{
tr_variant* args = nullptr;
int64_t ruleCount = 0;
if (!tr_variantDictFindDict(&response, TR_KEY_arguments, &args) ||
!tr_variantDictFindInt(args, TR_KEY_blocklist_size_kebab, &ruleCount))
{
ruleCount = -1;
}
if (ruleCount > 0)
{
gtr_pref_int_set(TR_KEY_blocklist_date, tr_time());
}
impl_->signal_blocklist_updated().emit(ruleCount >= 0);
});
}
/***
****
***/
void Session::exec(tr_variant const& request)
{
auto const tag = nextTag;
++nextTag;
impl_->send_rpc_request(request, tag, {});
}
/***
****
***/
size_t Session::get_torrent_count() const
{
return impl_->get_raw_model()->get_n_items();
}
size_t Session::get_active_torrent_count() const
{
return impl_->get_active_torrent_count();
}
size_t Session::Impl::get_active_torrent_count() const
{
size_t activeCount = 0;
for (auto i = 0U, count = raw_model_->get_n_items(); i < count; ++i)
{
if (raw_model_->get_item(i)->get_activity() != TR_STATUS_STOPPED)
{
++activeCount;
}
}
return activeCount;
}
tr_torrent* Session::find_torrent(tr_torrent_id_t id) const
{
tr_torrent* tor = nullptr;
if (auto* const session = impl_->get_session(); session != nullptr)
{
tor = tr_torrentFindFromId(session, id);
}
return tor;
}
FaviconCache<Glib::RefPtr<Gdk::Pixbuf>>& Session::favicon_cache() const
{
return impl_->favicon_cache();
}
void Session::open_folder(tr_torrent_id_t torrent_id) const
{
auto const* tor = find_torrent(torrent_id);
if (tor != nullptr)
{
bool const single = tr_torrentFileCount(tor) == 1;
char const* currentDir = tr_torrentGetCurrentDir(tor);
if (single)
{
gtr_open_file(currentDir);
}
else
{
gtr_open_file(Glib::build_filename(currentDir, tr_torrentName(tor)));
}
}
}
sigc::signal<void(Session::ErrorCode, Glib::ustring const&)>& Session::signal_add_error()
{
return impl_->signal_add_error();
}
sigc::signal<void(tr_ctor*)>& Session::signal_add_prompt()
{
return impl_->signal_add_prompt();
}
sigc::signal<void(bool)>& Session::signal_blocklist_updated()
{
return impl_->signal_blocklist_updated();
}
sigc::signal<void(bool)>& Session::signal_busy()
{
return impl_->signal_busy();
}
sigc::signal<void(tr_quark)>& Session::signal_prefs_changed()
{
return impl_->signal_prefs_changed();
}
sigc::signal<void(std::optional<bool>, Session::PortTestIpProtocol)>& Session::signal_port_tested()
{
return impl_->signal_port_tested();
}
sigc::signal<void(std::unordered_set<tr_torrent_id_t> const&, Torrent::ChangeFlags)>& Session::signal_torrents_changed()
{
return impl_->signal_torrents_changed();
}