feat: do separate IPv4 and IPv6 port checks in Qt and GTK Client (#6525)

* fix: specify `port-test` ip protocol in response when possible

* feat: IPv4 and IPv6 port test in Qt Client

* feat: shorten timeout of `port-test`

* feat: IPv4 and IPv6 port test in Gtk Client

* chore: housekeeping

* refactor: remove IP protocol error message

* code review: mikedld gtk

* feat: return tag in qt rpc response

* code review: mikedld qt

* feat: move port test button up alongside spin button

* fixup! code review: mikedld gtk

* fixup! code review: mikedld qt

* code review: port status initial text

* feat: decouple ipv4 and ipv6 status updates (GTK)

* feat: decouple ipv4 and ipv6 status updates (Qt)

* code review: unknown protocols are non-pending

Co-authored-by: Mike Gelfand <mikedld@users.noreply.github.com>

* code review: simplify status text when the statuses are the same

* Revert "feat: return tag in qt rpc response"

This reverts commit 2a022c2bb0ee7ddad81f8176839cf0d043422368.

* code review: add translation context for status text (GTK)

* code review: move `port_test_pending_` to `Impl` (GTK)

* fixup! code review: move `port_test_pending_` to `Impl` (GTK)

---------

Co-authored-by: Mike Gelfand <mikedld@users.noreply.github.com>
This commit is contained in:
Yat Ho 2024-01-22 06:50:26 +08:00 committed by GitHub
parent 29a566664a
commit 32ef92e7a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 337 additions and 116 deletions

View File

@ -673,7 +673,7 @@ Response arguments:
| Key | Value Type | Description
| :-- | :-- | :--
| `port-is-open` | boolean | true if port is open, false if port is closed
| `ipProtocol` | string | `ipv4` if the test was carried out on IPv4, `ipv6` if the test was carried out on IPv6, unset if an error occured
| `ipProtocol` | string | `ipv4` if the test was carried out on IPv4, `ipv6` if the test was carried out on IPv6, unset if it cannot be determined
### 4.5 Session shutdown
This method tells the transmission session to shut down.

View File

@ -42,11 +42,14 @@
#include <fmt/core.h>
#include <array>
#include <limits>
#include <map>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
using namespace libtransmission::Values;
@ -878,10 +881,23 @@ public:
TR_DISABLE_COPY_MOVE(NetworkPage)
private:
enum PortTestStatus : uint8_t
{
PORT_TEST_UNKNOWN = 0U,
PORT_TEST_CHECKING,
PORT_TEST_OPEN,
PORT_TEST_CLOSED,
PORT_TEST_ERROR
};
void portTestSetSensitive();
void updatePortStatusText();
void onCorePrefsChanged(tr_quark key);
void onPortTested(bool isOpen);
void onPortTested(std::optional<bool> result, Session::PortTestIpProtocol ip_protocol);
void onPortTest();
static std::string_view getPortStatusText(PortTestStatus status) noexcept;
private:
Glib::RefPtr<Session> core_;
@ -891,15 +907,18 @@ private:
sigc::connection portTag_;
sigc::connection prefsTag_;
std::array<PortTestStatus, Session::NUM_PORT_TEST_IP_PROTOCOL> portTestStatus_ = {};
};
void NetworkPage::onCorePrefsChanged(tr_quark const key)
{
if (key == TR_KEY_peer_port)
{
gtr_label_set_text(*portLabel_, _("Status unknown"));
portButton_->set_sensitive(true);
portSpin_->set_sensitive(true);
portTestStatus_[Session::PORT_TEST_IPV4] = PORT_TEST_UNKNOWN;
portTestStatus_[Session::PORT_TEST_IPV6] = PORT_TEST_UNKNOWN;
updatePortStatusText();
portTestSetSensitive();
}
}
@ -909,28 +928,79 @@ NetworkPage::~NetworkPage()
portTag_.disconnect();
}
void NetworkPage::onPortTested(bool isOpen)
std::string_view NetworkPage::getPortStatusText(PortTestStatus const status) noexcept
{
portLabel_->set_markup(fmt::format(
isOpen ? _("Port is {markup_begin}open{markup_end}") : _("Port is {markup_begin}closed{markup_end}"),
fmt::arg("markup_begin", "<b>"),
fmt::arg("markup_end", "</b>")));
portButton_->set_sensitive(true);
portSpin_->set_sensitive(true);
switch (status)
{
case PORT_TEST_UNKNOWN:
return C_("Port test status", "unknown");
case PORT_TEST_CHECKING:
return C_("Port test status", "checking…");
case PORT_TEST_OPEN:
return C_("Port test status", "open");
case PORT_TEST_CLOSED:
return C_("Port test status", "closed");
case PORT_TEST_ERROR:
return C_("Port test status", "error");
default:
return {};
}
}
void NetworkPage::updatePortStatusText()
{
auto const status_ipv4 = getPortStatusText(portTestStatus_[Session::PORT_TEST_IPV4]);
auto const status_ipv6 = getPortStatusText(portTestStatus_[Session::PORT_TEST_IPV6]);
portLabel_->set_markup(
portTestStatus_[Session::PORT_TEST_IPV4] == portTestStatus_[Session::PORT_TEST_IPV6] ?
fmt::format(_("Status: <b>{status}</b>"), fmt::arg("status", status_ipv4)) :
fmt::format(
_("Status: <b>{status_ipv4}</b> (IPv4), <b>{status_ipv6}</b> (IPv6)"),
fmt::arg("status_ipv4", status_ipv4),
fmt::arg("status_ipv6", status_ipv6)));
}
void NetworkPage::portTestSetSensitive()
{
// Depend on the RPC call status instead of the UI status, so that the widgets
// won't be enabled even if the port peer port changed while we have port-test
// RPC call(s) in-flight.
auto const sensitive = !core_->port_test_pending(Session::PORT_TEST_IPV4) &&
!core_->port_test_pending(Session::PORT_TEST_IPV6);
portButton_->set_sensitive(sensitive);
portSpin_->set_sensitive(sensitive);
}
void NetworkPage::onPortTested(std::optional<bool> const result, Session::PortTestIpProtocol const ip_protocol)
{
// Only update the UI if the current status is "checking", so that
// we won't show the port test results for the old peer port if it
// changed while we have port-test RPC call(s) in-flight.
if (portTestStatus_[ip_protocol] == PORT_TEST_CHECKING)
{
portTestStatus_[ip_protocol] = result ? (*result ? PORT_TEST_OPEN : PORT_TEST_CLOSED) : PORT_TEST_ERROR;
updatePortStatusText();
}
portTestSetSensitive();
}
void NetworkPage::onPortTest()
{
portButton_->set_sensitive(false);
portSpin_->set_sensitive(false);
portLabel_->set_text(_("Testing TCP port…"));
portTestStatus_[Session::PORT_TEST_IPV4] = PORT_TEST_CHECKING;
portTestStatus_[Session::PORT_TEST_IPV6] = PORT_TEST_CHECKING;
updatePortStatusText();
if (!portTag_.connected())
{
portTag_ = core_->signal_port_tested().connect([this](bool is_open) { onPortTested(is_open); });
portTag_ = core_->signal_port_tested().connect(
[this](std::optional<bool> status, Session::PortTestIpProtocol ip_protocol) { onPortTested(status, ip_protocol); });
}
core_->port_test();
core_->port_test(Session::PORT_TEST_IPV4);
core_->port_test(Session::PORT_TEST_IPV6);
portTestSetSensitive();
}
NetworkPage::NetworkPage(
@ -944,6 +1014,7 @@ NetworkPage::NetworkPage(
, portSpin_(init_spin_button("listening_port_spin", TR_KEY_peer_port, 1, std::numeric_limits<uint16_t>::max(), 1))
{
portButton_->signal_clicked().connect([this]() { onPortTest(); });
updatePortStatusText();
prefsTag_ = core_->signal_prefs_changed().connect([this](auto key) { onCorePrefsChanged(key); });

View File

@ -44,12 +44,14 @@
#include <fmt/core.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>
@ -74,6 +76,9 @@ public:
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();
@ -169,7 +174,7 @@ private:
sigc::signal<void(bool)> signal_blocklist_updated_;
sigc::signal<void(bool)> signal_busy_;
sigc::signal<void(tr_quark)> signal_prefs_changed_;
sigc::signal<void(bool)> signal_port_tested_;
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_;
@ -182,6 +187,7 @@ private:
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_;
@ -1220,33 +1226,67 @@ void Session::Impl::send_rpc_request(
**** Sending a test-port request via RPC
***/
void Session::port_test()
void Session::port_test(PortTestIpProtocol const ip_protocol)
{
auto const tag = nextTag;
++nextTag;
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_ipProtocol, 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));
tr_variant request;
tr_variantInitDict(&request, 2);
tr_variantDictAddStrView(&request, TR_KEY_method, "port-test");
tr_variantDictAddInt(&request, TR_KEY_tag, tag);
impl_->send_rpc_request(
request,
tr_variant{ std::move(request_map) },
tag,
[this](auto& response)
[this, ip_protocol](tr_variant& response)
{
tr_variant* args = nullptr;
bool is_open = false;
impl_->set_port_test_pending(false, ip_protocol);
if (!tr_variantDictFindDict(&response, TR_KEY_arguments, &args) ||
!tr_variantDictFindBool(args, TR_KEY_port_is_open, &is_open))
auto status = std::optional<bool>{};
if (tr_variant* args = nullptr; tr_variantDictFindDict(&response, TR_KEY_arguments, &args))
{
is_open = false;
if (auto result = bool{}; tr_variantDictFindBool(args, TR_KEY_port_is_open, &result))
{
status = result;
}
}
impl_->signal_port_tested().emit(is_open);
// 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
***/
@ -1386,7 +1426,7 @@ sigc::signal<void(tr_quark)>& Session::signal_prefs_changed()
return impl_->signal_prefs_changed();
}
sigc::signal<void(bool)>& Session::signal_port_tested()
sigc::signal<void(std::optional<bool>, Session::PortTestIpProtocol)>& Session::signal_port_tested()
{
return impl_->signal_port_tested();
}

View File

@ -23,6 +23,7 @@
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>
#include <vector>
@ -37,6 +38,13 @@ public:
ERR_NO_MORE_TORRENTS = 1000 /* finished adding a batch */
};
enum PortTestIpProtocol : uint8_t
{
PORT_TEST_IPV4,
PORT_TEST_IPV6,
NUM_PORT_TEST_IP_PROTOCOL // Must always be the last value
};
using Model = IF_GTKMM4(Gio::ListModel, Gtk::TreeModel);
public:
@ -128,7 +136,8 @@ public:
***
**/
void port_test();
void port_test(PortTestIpProtocol ip_protocol);
bool port_test_pending(PortTestIpProtocol ip_protocol) const noexcept;
void blocklist_update();
@ -141,7 +150,7 @@ public:
sigc::signal<void(bool)>& signal_blocklist_updated();
sigc::signal<void(bool)>& signal_busy();
sigc::signal<void(tr_quark)>& signal_prefs_changed();
sigc::signal<void(bool)>& signal_port_tested();
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();
protected:

View File

@ -1245,14 +1245,18 @@
</packing>
</child>
<child>
<object class="GtkSpinButton" id="listening_port_spin">
<object class="GtkLabel" id="listening_port_status_label">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="can-focus">False</property>
<property name="label">...</property>
<property name="xalign">0</property>
<attributes>
<attribute name="style" value="italic"/>
</attributes>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
@ -1262,14 +1266,10 @@
<property name="hexpand">True</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel" id="listening_port_status_label">
<object class="GtkSpinButton" id="listening_port_spin">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Status unknown</property>
<property name="xalign">0</property>
<attributes>
<attribute name="style" value="italic"/>
</attributes>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
</object>
<packing>
<property name="expand">True</property>
@ -1295,7 +1295,7 @@
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
</object>

View File

@ -868,12 +868,16 @@
</object>
</child>
<child>
<object class="GtkSpinButton" id="listening_port_spin">
<property name="focusable">1</property>
<object class="GtkLabel" id="listening_port_status_label">
<property name="hexpand">1</property>
<property name="label">...</property>
<property name="xalign">0</property>
<attributes>
<attribute name="style" value="italic"/>
</attributes>
<layout>
<property name="column">1</property>
<property name="row">0</property>
<property name="row">1</property>
</layout>
</object>
</child>
@ -882,13 +886,13 @@
<property name="hexpand">1</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel" id="listening_port_status_label">
<object class="GtkSpinButton" id="listening_port_spin">
<property name="focusable">1</property>
<property name="hexpand">1</property>
<property name="label" translatable="1">Status unknown</property>
<property name="xalign">0</property>
<attributes>
<attribute name="style" value="italic"></attribute>
</attributes>
<layout>
<property name="column">1</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
@ -901,7 +905,7 @@
</child>
<layout>
<property name="column">1</property>
<property name="row">1</property>
<property name="row">0</property>
</layout>
</object>
</child>

View File

@ -1144,6 +1144,12 @@ void onPortTested(tr_web::FetchResponse const& web_response)
auto const& [status, body, primary_ip, did_connect, did_timeout, user_data] = web_response;
auto* data = static_cast<tr_rpc_idle_data*>(user_data);
if (auto const addr = tr_address::from_string(primary_ip);
data->args_out.find_if<std::string_view>(TR_KEY_ipProtocol) == nullptr && addr && addr->is_valid())
{
data->args_out.try_emplace(TR_KEY_ipProtocol, addr->is_ipv4() ? "ipv4"sv : "ipv6"sv);
}
if (status != 200)
{
tr_idle_function_done(
@ -1155,30 +1161,30 @@ void onPortTested(tr_web::FetchResponse const& web_response)
return;
}
auto const addr = tr_address::from_string(primary_ip);
if (!addr || !addr->is_valid())
{
tr_idle_function_done(data, "Unknown error, please file a bug report to us");
return;
}
data->args_out.try_emplace(TR_KEY_port_is_open, tr_strv_starts_with(body, '1'));
data->args_out.try_emplace(TR_KEY_ipProtocol, addr->is_ipv4() ? "ipv4"sv : "ipv6"sv);
tr_idle_function_done(data, SuccessResult);
}
char const* portTest(tr_session* session, tr_variant::Map const& args_in, struct tr_rpc_idle_data* idle_data)
{
auto ip_proto = tr_web::FetchOptions::IPProtocol::ANY;
static auto constexpr TimeoutSecs = 20s;
auto const port = session->advertisedPeerPort();
auto const url = fmt::format("https://portcheck.transmissionbt.com/{:d}", port.host());
auto options = tr_web::FetchOptions{ url, onPortTested, idle_data };
options.timeout_secs = TimeoutSecs;
if (auto const* val = args_in.find_if<std::string_view>(TR_KEY_ipProtocol); val != nullptr)
{
if (*val == "ipv4"sv)
{
ip_proto = tr_web::FetchOptions::IPProtocol::V4;
options.ip_proto = tr_web::FetchOptions::IPProtocol::V4;
idle_data->args_out.try_emplace(TR_KEY_ipProtocol, "ipv4"sv);
}
else if (*val == "ipv6"sv)
{
ip_proto = tr_web::FetchOptions::IPProtocol::V6;
options.ip_proto = tr_web::FetchOptions::IPProtocol::V6;
idle_data->args_out.try_emplace(TR_KEY_ipProtocol, "ipv6"sv);
}
else
{
@ -1186,10 +1192,6 @@ char const* portTest(tr_session* session, tr_variant::Map const& args_in, struct
}
}
auto const port = session->advertisedPeerPort();
auto const url = fmt::format("https://portcheck.transmissionbt.com/{:d}", port.host());
auto options = tr_web::FetchOptions{ url, onPortTested, idle_data };
options.ip_proto = ip_proto;
session->fetch(std::move(options));
return nullptr;
}

View File

@ -6,6 +6,7 @@
#include "PrefsDialog.h"
#include <cassert>
#include <optional>
#include <QCheckBox>
#include <QComboBox>
@ -426,19 +427,70 @@ void PrefsDialog::initDesktopTab()
// ---
void PrefsDialog::onPortTested(bool isOpen)
QString PrefsDialog::getPortStatusText(PrefsDialog::PortTestStatus status) noexcept
{
ui_.testPeerPortButton->setEnabled(true);
widgets_[Prefs::PEER_PORT]->setEnabled(true);
ui_.peerPortStatusLabel->setText(isOpen ? tr("Port is <b>open</b>") : tr("Port is <b>closed</b>"));
switch (status)
{
case PORT_TEST_UNKNOWN:
return tr("unknown");
case PORT_TEST_CHECKING:
return tr("checking…");
case PORT_TEST_OPEN:
return tr("open");
case PORT_TEST_CLOSED:
return tr("closed");
case PORT_TEST_ERROR:
return tr("error");
default:
return {};
}
}
void PrefsDialog::updatePortStatusLabel()
{
auto const status_ipv4 = getPortStatusText(port_test_status_[Session::PORT_TEST_IPV4]);
auto const status_ipv6 = getPortStatusText(port_test_status_[Session::PORT_TEST_IPV6]);
ui_.peerPortStatusLabel->setText(
port_test_status_[Session::PORT_TEST_IPV4] == port_test_status_[Session::PORT_TEST_IPV6] ?
tr("Status: <b>%1</b>").arg(status_ipv4) :
tr("Status: <b>%1</b> (IPv4), <b>%2</b> (IPv6)").arg(status_ipv4).arg(status_ipv6));
}
void PrefsDialog::portTestSetEnabled()
{
// Depend on the RPC call status instead of the UI status, so that the widgets
// won't be enabled even if the port peer port changed while we have port-test
// RPC call(s) in-flight.
auto const sensitive = !session_.portTestPending(Session::PORT_TEST_IPV4) &&
!session_.portTestPending(Session::PORT_TEST_IPV6);
ui_.testPeerPortButton->setEnabled(sensitive);
widgets_[Prefs::PEER_PORT]->setEnabled(sensitive);
}
void PrefsDialog::onPortTested(std::optional<bool> result, Session::PortTestIpProtocol ip_protocol)
{
// Only update the UI if the current status is "checking", so that
// we won't show the port test results for the old peer port if it
// changed while we have port-test RPC call(s) in-flight.
if (port_test_status_[ip_protocol] == PORT_TEST_CHECKING)
{
port_test_status_[ip_protocol] = result ? (*result ? PORT_TEST_OPEN : PORT_TEST_CLOSED) : PORT_TEST_ERROR;
updatePortStatusLabel();
}
portTestSetEnabled();
}
void PrefsDialog::onPortTest()
{
ui_.peerPortStatusLabel->setText(tr("Testing TCP Port…"));
ui_.testPeerPortButton->setEnabled(false);
widgets_[Prefs::PEER_PORT]->setEnabled(false);
session_.portTest();
port_test_status_[Session::PORT_TEST_IPV4] = PORT_TEST_CHECKING;
port_test_status_[Session::PORT_TEST_IPV6] = PORT_TEST_CHECKING;
updatePortStatusLabel();
session_.portTest(Session::PORT_TEST_IPV4);
session_.portTest(Session::PORT_TEST_IPV6);
portTestSetEnabled();
}
void PrefsDialog::initNetworkTab()
@ -464,6 +516,8 @@ void PrefsDialog::initNetworkTab()
connect(ui_.testPeerPortButton, &QAbstractButton::clicked, this, &PrefsDialog::onPortTest);
connect(&session_, &Session::portTested, this, &PrefsDialog::onPortTested);
updatePortStatusLabel();
}
// ---
@ -781,8 +835,10 @@ void PrefsDialog::refreshPref(int key)
}
case Prefs::PEER_PORT:
ui_.peerPortStatusLabel->setText(tr("Status unknown"));
ui_.testPeerPortButton->setEnabled(true);
port_test_status_[Session::PORT_TEST_IPV4] = PORT_TEST_UNKNOWN;
port_test_status_[Session::PORT_TEST_IPV6] = PORT_TEST_UNKNOWN;
updatePortStatusLabel();
portTestSetEnabled();
break;
default:

View File

@ -5,21 +5,21 @@
#pragma once
#include <array>
#include <map>
#include <optional>
#include <libtransmission/tr-macros.h>
#include "BaseDialog.h"
#include "Prefs.h"
#include "Session.h"
#include "ui_PrefsDialog.h"
class QHttp;
class QMessageBox;
class QString;
class Prefs;
class Session;
class PrefsDialog : public BaseDialog
{
Q_OBJECT
@ -39,7 +39,7 @@ private slots:
void encryptionEdited(int);
void altSpeedDaysEdited(int);
void sessionUpdated();
void onPortTested(bool);
void onPortTested(std::optional<bool>, Session::PortTestIpProtocol);
void onPortTest();
void onIdleLimitChanged();
void onQueueStalledMinutesChanged();
@ -52,11 +52,23 @@ private slots:
private:
using key2widget_t = std::map<int, QWidget*>;
enum PortTestStatus : uint8_t
{
PORT_TEST_UNKNOWN = 0U,
PORT_TEST_CHECKING,
PORT_TEST_OPEN,
PORT_TEST_CLOSED,
PORT_TEST_ERROR
};
bool updateWidgetValue(QWidget* widget, int pref_key) const;
void portTestSetEnabled();
void linkWidgetToPref(QWidget* widget, int pref_key);
void updateBlocklistLabel();
void updateDownloadingWidgetsLocality();
void updatePortStatusLabel();
void updateSeedingWidgetsLocality();
static QString getPortStatusText(PortTestStatus status) noexcept;
void setPref(int key, QVariant const& v);
@ -75,6 +87,7 @@ private:
bool const is_server_;
bool is_local_ = {};
std::array<PortTestStatus, Session::NUM_PORT_TEST_IP_PROTOCOL> port_test_status_ = {};
key2widget_t widgets_;
QWidgetList web_widgets_;

View File

@ -650,7 +650,7 @@
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<item row="0" column="1">
<widget class="QSpinBox" name="peerPortSpin">
<property name="minimum">
<number>1</number>
@ -660,14 +660,7 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="peerPortStatusLabel">
<property name="text">
<string>Status unknown</string>
</property>
</widget>
</item>
<item row="1" column="2">
<item row="0" column="2">
<widget class="QPushButton" name="testPeerPortButton">
<property name="text">
<string>Te&amp;st Port</string>
@ -677,6 +670,13 @@
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QLabel" name="peerPortStatusLabel">
<property name="text">
<string notr="true">...</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="3">
<widget class="QCheckBox" name="randomPeerPortCheck">
<property name="text">

View File

@ -6,6 +6,7 @@
#include <algorithm>
#include <array>
#include <cassert>
#include <optional>
#include <string_view>
#include <utility>
@ -39,6 +40,8 @@
#include "Torrent.h"
#include "VariantHelpers.h"
using namespace std::literals;
using ::trqt::variant_helpers::dictAdd;
using ::trqt::variant_helpers::dictFind;
using ::trqt::variant_helpers::getValue;
@ -81,32 +84,43 @@ void Session::sessionSet(tr_quark const key, QVariant const& value)
exec("session-set", &args);
}
void Session::portTest()
void Session::portTest(Session::PortTestIpProtocol const ip_protocol)
{
static auto constexpr IpStr = std::array{ "ipv4"sv, "ipv6"sv };
if (portTestPending(ip_protocol))
{
return;
}
port_test_pending_[ip_protocol] = true;
auto args = tr_variant::make_map(1U);
tr_variantDictAddStrView(&args, TR_KEY_ipProtocol, IpStr[ip_protocol]);
auto const response_func = [this, ip_protocol](RpcResponse const& r)
{
port_test_pending_[ip_protocol] = false;
// 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
emit portTested(dictFind<bool>(r.args.get(), TR_KEY_port_is_open), ip_protocol);
};
auto* q = new RpcQueue{};
q->add([this]() { return exec("port-test", nullptr); });
q->add([this, &args]() { return exec("port-test", &args); }, response_func);
q->add(
[this](RpcResponse const& r)
{
bool is_open = false;
if (r.success)
{
auto const value = dictFind<bool>(r.args.get(), TR_KEY_port_is_open);
if (value)
{
is_open = *value;
}
}
emit portTested(is_open);
});
q->add(response_func);
q->run();
}
bool Session::portTestPending(Session::PortTestIpProtocol const ip_protocol) const noexcept
{
return ip_protocol < NUM_PORT_TEST_IP_PROTOCOL && port_test_pending_[ip_protocol];
}
void Session::copyMagnetLinkToClipboard(int torrent_id)
{
auto constexpr MagnetLinkKey = std::string_view{ "magnetLink" };

View File

@ -5,8 +5,10 @@
#pragma once
#include <array>
#include <cstdint> // int64_t
#include <map>
#include <optional>
#include <string_view>
#include <vector>
@ -69,11 +71,20 @@ public:
return blocklist_size_;
}
enum PortTestIpProtocol : uint8_t
{
PORT_TEST_IPV4,
PORT_TEST_IPV6,
NUM_PORT_TEST_IP_PROTOCOL
};
void setBlocklistSize(int64_t i);
void updateBlocklist();
void portTest();
void portTest(PortTestIpProtocol ip_protocol);
void copyMagnetLinkToClipboard(int torrent_id);
bool portTestPending(PortTestIpProtocol ip_protocol) const noexcept;
/** returns true if the transmission session is being run inside this client */
bool isServer() const;
@ -130,7 +141,7 @@ public slots:
signals:
void sourceChanged();
void portTested(bool is_open);
void portTested(std::optional<bool> status, PortTestIpProtocol ip_protocol);
void statsUpdated();
void sessionUpdated();
void blocklistUpdated(int);
@ -168,6 +179,7 @@ private:
std::map<TorrentProperties, std::vector<std::string_view>> names_;
int64_t blocklist_size_ = -1;
std::array<bool, NUM_PORT_TEST_IP_PROTOCOL> port_test_pending_ = {};
tr_session* session_ = {};
QStringList idle_json_;
tr_session_stats stats_ = EmptyStats;