mirror of
https://github.com/rakshasa/rtorrent.git
synced 2026-10-05 13:49:21 +00:00
Compare commits
16 Commits
v0.16.23
...
b1be898ba1
| Author | SHA1 | Date | |
|---|---|---|---|
| b1be898ba1 | |||
| bd00b5f6fe | |||
| b34eb1e895 | |||
| b857acd081 | |||
| 988aca1281 | |||
| 378c7ee854 | |||
| da9606bfae | |||
| 8423803b6e | |||
| c1ce2febf0 | |||
| 5ebb7bd151 | |||
| 50f0cad0c6 | |||
| 3717649bb3 | |||
| c32b56e9d8 | |||
| de54a4c2ad | |||
| 26fdca8f4d | |||
| 00bcdaee23 |
@@ -431,6 +431,9 @@ p_call_target(const torrent::Object::list_type& args) {
|
||||
const std::string& peer_id = itr++->as_string();
|
||||
const std::string& command_key = itr++->as_string();
|
||||
|
||||
if (download == nullptr)
|
||||
throw torrent::input_error("invalid parameters: info-hash not found");
|
||||
|
||||
torrent::HashString hash;
|
||||
|
||||
if (peer_id.size() != 40)
|
||||
|
||||
@@ -157,7 +157,7 @@ system_method_insert_object(const torrent::Object::list_type& args, int flags) {
|
||||
torrent::Object valueList = torrent::Object::create_list();
|
||||
torrent::Object::list_type& valueListType = valueList.as_list();
|
||||
|
||||
if ((itrArgs)->is_list())
|
||||
if (itrArgs != args.end() && (itrArgs)->is_list())
|
||||
valueListType = (itrArgs)->as_list();
|
||||
|
||||
control->object_storage()->insert_str(raw_key, valueList, flags);
|
||||
|
||||
@@ -319,6 +319,11 @@ apply_ipv4_filter_load(const torrent::Object::list_type& args) {
|
||||
ipv4_filter_parse(buffer, value);
|
||||
}
|
||||
|
||||
if (file.fail() && !file.eof()) {
|
||||
lineNumber++;
|
||||
throw torrent::input_error("Exceeded max line length.");
|
||||
}
|
||||
|
||||
} catch (torrent::input_error& e) {
|
||||
snprintf(buffer, 2048, "Error in ip filter file: %s:%u: %s", filename.c_str(), lineNumber, e.what());
|
||||
|
||||
|
||||
+15
-9
@@ -374,25 +374,31 @@ initialize_command_network() {
|
||||
CMD_ANY_STRING ("network.tos.set", [](auto, auto& str) { return apply_tos(str); });
|
||||
|
||||
CMD_ANY ("network.bind_address", [nw_config](auto, auto) { return nw_config->bind_address_best_match_str(); });
|
||||
CMD_ANY_STRING_V("network.bind_address.set", [nw_config](auto, auto& str) { return nw_config->set_bind_address_str(str); });
|
||||
CMD_ANY_STRING_V("network.bind_address.set", [nw_config](auto, auto& str) { return nw_config->set_bind_address(str); });
|
||||
CMD_ANY ("network.bind_address.ipv4", [nw_config](auto, auto) { return nw_config->bind_inet_address_str(); });
|
||||
CMD_ANY_STRING_V("network.bind_address.ipv4.set", [nw_config](auto, auto& str) { return nw_config->set_bind_inet_address_str(str); });
|
||||
CMD_ANY_STRING_V("network.bind_address.ipv4.set", [nw_config](auto, auto& str) { return nw_config->set_bind_inet_address(str); });
|
||||
CMD_ANY ("network.bind_address.ipv6", [nw_config](auto, auto) { return nw_config->bind_inet6_address_str(); });
|
||||
CMD_ANY_STRING_V("network.bind_address.ipv6.set", [nw_config](auto, auto& str) { return nw_config->set_bind_inet6_address_str(str); });
|
||||
CMD_ANY_STRING_V("network.bind_address.ipv6.set", [nw_config](auto, auto& str) { return nw_config->set_bind_inet6_address(str); });
|
||||
|
||||
CMD_ANY_STRING_V("network.bind_device.set", [nw_config](auto, auto& str) { return nw_config->set_bind_device_name(str); });
|
||||
CMD_ANY ("network.bind_device.ipv4", [nw_config](auto, auto) { return nw_config->bind_inet_device_name(); });
|
||||
CMD_ANY_STRING_V("network.bind_device.ipv4.set", [nw_config](auto, auto& str) { return nw_config->set_bind_inet_device_name(str); });
|
||||
CMD_ANY ("network.bind_device.ipv6", [nw_config](auto, auto) { return nw_config->bind_inet6_device_name(); });
|
||||
CMD_ANY_STRING_V("network.bind_device.ipv6.set", [nw_config](auto, auto& str) { return nw_config->set_bind_inet6_device_name(str); });
|
||||
|
||||
CMD_ANY ("network.local_address", [nw_config](auto, auto) { return nw_config->local_address_best_match_str(); });
|
||||
CMD_ANY_STRING_V("network.local_address.set", [nw_config](auto, auto& str) { return nw_config->set_local_address_str(str); });
|
||||
CMD_ANY_STRING_V("network.local_address.set", [nw_config](auto, auto& str) { return nw_config->set_local_address(str); });
|
||||
CMD_ANY ("network.local_address.ipv4", [nw_config](auto, auto) { return nw_config->local_inet_address_str(); });
|
||||
CMD_ANY_STRING_V("network.local_address.ipv4.set", [nw_config](auto, auto& str) { return nw_config->set_local_inet_address_str(str); });
|
||||
CMD_ANY_STRING_V("network.local_address.ipv4.set", [nw_config](auto, auto& str) { return nw_config->set_local_inet_address(str); });
|
||||
CMD_ANY ("network.local_address.ipv6", [nw_config](auto, auto) { return nw_config->local_inet6_address_str(); });
|
||||
CMD_ANY_STRING_V("network.local_address.ipv6.set", [nw_config](auto, auto& str) { return nw_config->set_local_inet6_address_str(str); });
|
||||
CMD_ANY_STRING_V("network.local_address.ipv6.set", [nw_config](auto, auto& str) { return nw_config->set_local_inet6_address(str); });
|
||||
|
||||
CMD_ANY ("network.local_port", [nw_config](auto, auto) { return nw_config->local_port_best_match(); });
|
||||
CMD_ANY_VALUE_V ("network.local_port.set", [nw_config](auto, auto& value) { return nw_config->set_local_port(checked_local_port_value(value, "local")); });
|
||||
CMD_ANY_VALUE_V ("network.local_port.set", [nw_config](auto, auto& value) { return nw_config->set_local_port(checked_local_port_value(value, "local")); });
|
||||
CMD_ANY ("network.local_port.ipv4", [nw_config](auto, auto) { return nw_config->local_inet_port(); });
|
||||
CMD_ANY_VALUE_V ("network.local_port.ipv4.set", [nw_config](auto, auto& value) { return nw_config->set_local_inet_port(checked_local_port_value(value, "local ipv4")); });
|
||||
CMD_ANY_VALUE_V ("network.local_port.ipv4.set", [nw_config](auto, auto& value) { return nw_config->set_local_inet_port(checked_local_port_value(value, "local ipv4")); });
|
||||
CMD_ANY ("network.local_port.ipv6", [nw_config](auto, auto) { return nw_config->local_inet6_port(); });
|
||||
CMD_ANY_VALUE_V ("network.local_port.ipv6.set", [nw_config](auto, auto& value) { return nw_config->set_local_inet6_port(checked_local_port_value(value, "local ipv6")); });
|
||||
CMD_ANY_VALUE_V ("network.local_port.ipv6.set", [nw_config](auto, auto& value) { return nw_config->set_local_inet6_port(checked_local_port_value(value, "local ipv6")); });
|
||||
|
||||
CMD_ANY ("network.proxy.global", [](auto, auto) { return torrent::runtime::proxy_manager()->proxy_url(); });
|
||||
CMD_ANY_STRING_V("network.proxy.global.set", [](auto, auto& str) { return torrent::runtime::proxy_manager()->set_proxy_url(str); });
|
||||
|
||||
@@ -83,7 +83,9 @@ apply_throttle(const torrent::Object::list_type& args, bool up) {
|
||||
throw torrent::input_error("Missing throttle rate for '" + name + "'.");
|
||||
|
||||
int64_t rate;
|
||||
rpc::parse_whole_value_nothrow(arg_itr->as_string().c_str(), &rate);
|
||||
|
||||
if (!rpc::parse_whole_value_nothrow(arg_itr->as_string().c_str(), &rate))
|
||||
throw torrent::input_error("Invalid throttle rate for '" + name + "'.");
|
||||
|
||||
if (rate < 0)
|
||||
throw torrent::input_error("Throttle rate must be non-negative.");
|
||||
|
||||
@@ -132,11 +132,12 @@ DhtManager::save_dht_cache() {
|
||||
torrent::Object cache = torrent::Object::create_map();
|
||||
cache_file << *torrent::runtime::network_manager()->dht_controller()->store_cache(&cache);
|
||||
|
||||
// The data only reaches the kernel here, so this is where a full disk is seen.
|
||||
cache_file.close();
|
||||
|
||||
if (!cache_file.good())
|
||||
return;
|
||||
|
||||
cache_file.close();
|
||||
|
||||
::rename(filename_tmp.c_str(), filename.c_str());
|
||||
}
|
||||
|
||||
|
||||
@@ -301,7 +301,7 @@ DownloadList::set_state_stopped(Download* download) {
|
||||
void
|
||||
DownloadList::update_paused_state(Download* download) {
|
||||
rpc::call_command("d.state_changed.set", torrent::this_thread::cached_seconds().count(), rpc::make_target(download));
|
||||
rpc::call_command("d.state_counter.set", rpc::call_command_value("d.state_counter", rpc::make_target(download)), rpc::make_target(download));
|
||||
rpc::call_command("d.state_counter.set", rpc::call_command_value("d.state_counter", rpc::make_target(download)) + 1, rpc::make_target(download));
|
||||
|
||||
// If initial seeding is complete, don't try it again when restarting.
|
||||
if (download->is_done() &&
|
||||
@@ -726,11 +726,10 @@ DownloadList::process_meta_download(Download* download) {
|
||||
return;
|
||||
}
|
||||
|
||||
torrent::Object* bencode = new torrent::Object(torrent::Object::create_map());
|
||||
auto bencode = std::make_unique<torrent::Object>(torrent::Object::create_map());
|
||||
file >> bencode->insert_key("info", torrent::Object());
|
||||
|
||||
if (file.fail()) {
|
||||
delete bencode;
|
||||
lt_log_print(torrent::LOG_TORRENT_ERROR, "Could not create download, the input is not a valid torrent.");
|
||||
return;
|
||||
}
|
||||
@@ -746,7 +745,7 @@ DownloadList::process_meta_download(Download* download) {
|
||||
|
||||
erase_ptr(download);
|
||||
|
||||
control->core()->try_create_download_from_meta_download(bencode, metafile);
|
||||
control->core()->try_create_download_from_meta_download(std::move(bencode), metafile);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-3
@@ -195,14 +195,14 @@ Manager::try_create_download(const std::string& uri, int flags, const command_li
|
||||
return;
|
||||
|
||||
// Adding download.
|
||||
DownloadFactory* f = new DownloadFactory(this);
|
||||
auto f = std::make_unique<DownloadFactory>(this);
|
||||
|
||||
f->variables()["tied_to_file"] = (int64_t)(bool)(flags & create_tied);
|
||||
f->commands().insert(f->commands().end(), commands.begin(), commands.end());
|
||||
|
||||
f->set_start(flags & create_start);
|
||||
f->set_print_log(!(flags & create_quiet));
|
||||
f->slot_finished([f]() { delete f; });
|
||||
f->slot_finished([factory = f.get()]() { delete factory; });
|
||||
|
||||
if (is_data_uri(uri)) {
|
||||
// Allow the use of data URIs, primarily for JSON-RPC which
|
||||
@@ -216,10 +216,13 @@ Manager::try_create_download(const std::string& uri, int flags, const command_li
|
||||
}
|
||||
|
||||
f->commit();
|
||||
|
||||
// From here the finished slot deletes it.
|
||||
f.release();
|
||||
}
|
||||
|
||||
void
|
||||
Manager::try_create_download_from_meta_download(torrent::Object* bencode, const std::string& metafile) {
|
||||
Manager::try_create_download_from_meta_download(std::unique_ptr<torrent::Object> bencode, const std::string& metafile) {
|
||||
DownloadFactory* f = new DownloadFactory(this);
|
||||
|
||||
f->variables()["tied_to_file"] = (int64_t)true;
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ public:
|
||||
// Temporary, find a better place for this.
|
||||
void try_create_download(const std::string& uri, int flags, const command_list_type& commands);
|
||||
void try_create_download_expand(const std::string& uri, int flags, command_list_type commands = command_list_type());
|
||||
void try_create_download_from_meta_download(torrent::Object* bencode, const std::string& metafile);
|
||||
void try_create_download_from_meta_download(std::unique_ptr<torrent::Object> bencode, const std::string& metafile);
|
||||
|
||||
private:
|
||||
void create_http(const std::string& uri);
|
||||
|
||||
+2
-2
@@ -216,7 +216,7 @@ View::set_not_visible(Download* download) {
|
||||
|
||||
void
|
||||
View::next_focus(unsigned int i) {
|
||||
if (empty())
|
||||
if (empty_visible())
|
||||
return;
|
||||
|
||||
// If at the boundary, roll over
|
||||
@@ -239,7 +239,7 @@ View::next_focus(unsigned int i) {
|
||||
|
||||
void
|
||||
View::prev_focus(unsigned int i) {
|
||||
if (empty())
|
||||
if (empty_visible())
|
||||
return;
|
||||
|
||||
// If at the boundary, roll over
|
||||
|
||||
@@ -25,7 +25,10 @@ WindowFileList::WindowFileList(const ui::ElementFileList* element) :
|
||||
// Convert std::string to std::wstring of given width (in screen positions),
|
||||
// taking into account that some characters may be occupying two screen positions.
|
||||
std::wstring
|
||||
wstring_width(const std::string& i_str, [[maybe_unused]] int width) {
|
||||
wstring_width(const std::string& i_str, int width) {
|
||||
if (width < 0)
|
||||
width = 0;
|
||||
|
||||
std::wstring result(width + 1, L' ');
|
||||
size_t length = std::mbstowcs(result.data(), i_str.c_str(), width);
|
||||
|
||||
|
||||
@@ -61,14 +61,20 @@ CommandScheduler::call_item(value_type item) {
|
||||
// Remove the item before calling the command if it should be
|
||||
// removed.
|
||||
|
||||
std::string key = item->key();
|
||||
|
||||
try {
|
||||
rpc::call_object(item->command());
|
||||
|
||||
} catch (torrent::input_error& e) {
|
||||
if (m_slotErrorMessage)
|
||||
m_slotErrorMessage("Scheduled command failed: " + item->key() + ": " + e.what());
|
||||
m_slotErrorMessage("Scheduled command failed: " + key + ": " + e.what());
|
||||
}
|
||||
|
||||
// The command is allowed to erase or replace this item, which deletes it.
|
||||
if (std::find(begin(), end(), item) == end())
|
||||
return;
|
||||
|
||||
// Still schedule if we caught a torrrent::input_error?
|
||||
auto next = item->next_time_scheduled();
|
||||
|
||||
|
||||
@@ -64,8 +64,12 @@ object_storage::insert(const char* key_data, uint32_t key_size, const torrent::O
|
||||
if (std::find(key_data, key_data + key_size, '\0') != key_data + key_size)
|
||||
throw torrent::input_error("Found nul-char in string.");
|
||||
|
||||
// Check for size > key_size.
|
||||
// Check for empty string.
|
||||
// key_type turns a key this long into the empty key.
|
||||
if (key_size >= object_storage::key_size)
|
||||
throw torrent::input_error("Key is too long.");
|
||||
|
||||
if (key_size == 0)
|
||||
throw torrent::input_error("Key is empty.");
|
||||
|
||||
bool use_raw = false;
|
||||
torrent::Object object;
|
||||
|
||||
@@ -181,6 +181,11 @@ parse_command_file(const std::string& path) {
|
||||
getCount = 0;
|
||||
}
|
||||
|
||||
if (file.fail() && !file.eof()) {
|
||||
lineNumber++;
|
||||
throw torrent::input_error("Exceeded max line length.");
|
||||
}
|
||||
|
||||
} catch (torrent::input_error& e) {
|
||||
snprintf(buffer, 2048, "Error in option file: %s:%u: %s", path.c_str(), lineNumber, e.what());
|
||||
|
||||
|
||||
@@ -93,7 +93,8 @@ SCgiTask::close() {
|
||||
// The callbacks are guaranteed to be finished/canceled at this point.
|
||||
auto lock = std::lock_guard<std::mutex>(m_result_mutex);
|
||||
|
||||
m_buffer.clear();
|
||||
// clear() would keep the capacity, and the task is pooled for the lifetime of the process.
|
||||
std::vector<char>().swap(m_buffer);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
+4
-5
@@ -44,10 +44,9 @@ xmlrpc_list_entry_to_object(xmlrpc_env* env, xmlrpc_value* src, int index) {
|
||||
if (env->fault_occurred)
|
||||
throw xmlrpc_error_c(env);
|
||||
|
||||
torrent::Object obj = xmlrpc_to_object(env, tmp);
|
||||
xmlrpc_DECREF(tmp);
|
||||
utils::scope_guard guard([tmp]() { xmlrpc_DECREF(tmp); });
|
||||
|
||||
return obj;
|
||||
return xmlrpc_to_object(env, tmp);
|
||||
}
|
||||
|
||||
int64_t
|
||||
@@ -213,11 +212,11 @@ xmlrpc_to_object(xmlrpc_env* env, xmlrpc_value* value, int call_type, rpc::targe
|
||||
if (env->fault_occurred)
|
||||
throw xmlrpc_error_c(env);
|
||||
|
||||
utils::scope_guard guard([tmp]() { xmlrpc_DECREF(tmp); });
|
||||
|
||||
if (target != nullptr)
|
||||
std::tie(*target, *deleter) = xmlrpc_to_target(env, tmp, call_type);
|
||||
|
||||
xmlrpc_DECREF(tmp);
|
||||
|
||||
if (env->fault_occurred)
|
||||
throw xmlrpc_error_c(env);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "rpc/tinyxml2/tinyxml2.h"
|
||||
#include "rpc/rpc_manager.h"
|
||||
#include "utils/base64.h"
|
||||
#include "utils/functional.h"
|
||||
#include "xmlrpc.h"
|
||||
|
||||
namespace rpc {
|
||||
@@ -258,6 +259,8 @@ execute_command(std::string method_name, const tinyxml2::XMLElement* params_elem
|
||||
torrent::Object params_raw = torrent::Object::create_list();
|
||||
torrent::Object::list_type& params = params_raw.as_list();
|
||||
rpc::target_type target = rpc::make_target();
|
||||
std::function<void()> deleter = []() {};
|
||||
utils::scope_guard guard([&deleter]() { deleter(); });
|
||||
|
||||
if (params_element != nullptr) {
|
||||
if (std::strncmp(params_element->Name(), "params", sizeof("params")) == 0) {
|
||||
@@ -265,8 +268,6 @@ execute_command(std::string method_name, const tinyxml2::XMLElement* params_elem
|
||||
const auto* child = params_element->FirstChildElement("param");
|
||||
|
||||
if (child != nullptr) {
|
||||
std::function<void()> deleter = []() {};
|
||||
|
||||
RpcManager::object_to_target(xml_value_to_object(child->FirstChildElement("value")), cmd_itr->second.m_flags, &target, &deleter);
|
||||
child = child->NextSiblingElement("param");
|
||||
|
||||
@@ -282,8 +283,6 @@ execute_command(std::string method_name, const tinyxml2::XMLElement* params_elem
|
||||
const auto* child = params_element->FirstChildElement("data")->FirstChildElement("value");
|
||||
|
||||
if (child != nullptr) {
|
||||
std::function<void()> deleter = []() {};
|
||||
|
||||
RpcManager::object_to_target(xml_value_to_object(child), cmd_itr->second.m_flags, &target, &deleter);
|
||||
child = child->NextSiblingElement("value");
|
||||
|
||||
|
||||
+3
-2
@@ -467,6 +467,9 @@ Root::save_input_history() {
|
||||
history_file << entry << "|" + category.at((pitr->second + i) % m_input_history_length) + "\n";
|
||||
}
|
||||
|
||||
// The data only reaches the kernel here, so this is where a full disk is seen.
|
||||
history_file.close();
|
||||
|
||||
if (!history_file.good()) {
|
||||
lt_log_print(torrent::LOG_DEBUG, "input history file corrupted during writing, discarding (path:%s)", history_filename.c_str());
|
||||
return;
|
||||
@@ -474,8 +477,6 @@ Root::save_input_history() {
|
||||
lt_log_print(torrent::LOG_DEBUG, "input history file written (path:%s)", history_filename.c_str());
|
||||
}
|
||||
|
||||
history_file.close();
|
||||
|
||||
std::rename(history_filename_tmp.c_str(), history_filename.c_str());
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,17 @@ TestObjectStorage::test_validate_keys() {
|
||||
torrent::raw_string raw_string_4("test_4\0foo", 10);
|
||||
|
||||
ASSERT_CATCH_INPUT_ERROR( { m_storage.insert(raw_string_4, torrent::Object("a"), rpc::object_storage::flag_string_type); } );
|
||||
ASSERT_CATCH_INPUT_ERROR( { m_storage.insert_str("", torrent::Object("a"), rpc::object_storage::flag_string_type); } );
|
||||
|
||||
std::string key_max(rpc::object_storage::key_size - 1, 'k');
|
||||
|
||||
CPPUNIT_ASSERT(m_storage.insert_str(key_max, torrent::Object("a"), rpc::object_storage::flag_string_type)->first == key_max);
|
||||
ASSERT_CATCH_INPUT_ERROR( { m_storage.insert_str(key_max + 'k', torrent::Object("a"), rpc::object_storage::flag_string_type); } );
|
||||
|
||||
// The over-long key must not have been stored as the empty key.
|
||||
CPPUNIT_ASSERT(m_storage.find_raw_string(torrent::raw_string::from_c_str("")) == m_storage.end());
|
||||
|
||||
m_storage.clear();
|
||||
}
|
||||
|
||||
// And test many other bad/good string combos.
|
||||
|
||||
@@ -61,3 +61,24 @@ TestCommandDynamic::test_old_style() {
|
||||
rpc::commands.call_command("method.insert", rpc::create_object_list("test_old_style.4", "simple", "cat=test.3"));
|
||||
CPPUNIT_ASSERT(rpc::commands.call_command("test_old_style.4", torrent::Object()).as_string() == "test.3");
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandDynamic::test_insert_list() {
|
||||
torrent::Object key_only = torrent::Object::create_list();
|
||||
key_only.as_list().push_back("test_insert_list.1");
|
||||
|
||||
rpc::commands.call_command("method.insert.list", key_only);
|
||||
|
||||
torrent::Object result = rpc::commands.call_command("test_insert_list.1", torrent::Object());
|
||||
|
||||
CPPUNIT_ASSERT(result.is_list());
|
||||
CPPUNIT_ASSERT(result.as_list().empty());
|
||||
|
||||
rpc::commands.call_command("method.insert.list",
|
||||
rpc::create_object_list("test_insert_list.2", rpc::create_object_list("a", "b")));
|
||||
|
||||
torrent::Object filled = rpc::commands.call_command("test_insert_list.2", torrent::Object());
|
||||
|
||||
CPPUNIT_ASSERT(filled.is_list());
|
||||
CPPUNIT_ASSERT_EQUAL((size_t)2, filled.as_list().size());
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ class TestCommandDynamic : public test_fixture {
|
||||
CPPUNIT_TEST(test_basics);
|
||||
CPPUNIT_TEST(test_get_set);
|
||||
CPPUNIT_TEST(test_old_style);
|
||||
CPPUNIT_TEST(test_insert_list);
|
||||
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
@@ -18,6 +19,7 @@ public:
|
||||
void test_get_set();
|
||||
|
||||
void test_old_style();
|
||||
void test_insert_list();
|
||||
|
||||
private:
|
||||
std::unique_ptr<TestMainThread> m_test_main_thread;
|
||||
|
||||
Reference in New Issue
Block a user