2018-12-31 20:04:05 +01:00
|
|
|
//
|
2021-01-01 13:57:46 +01:00
|
|
|
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2021
|
2018-12-31 20:04:05 +01:00
|
|
|
//
|
|
|
|
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
|
|
|
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
|
|
|
//
|
|
|
|
#include "td/telegram/Client.h"
|
|
|
|
|
|
|
|
#include "td/telegram/Td.h"
|
2020-07-30 03:04:57 +02:00
|
|
|
#include "td/telegram/TdCallback.h"
|
2020-10-13 03:11:19 +02:00
|
|
|
#include "td/telegram/Log.h"
|
2018-12-31 20:04:05 +01:00
|
|
|
|
2018-07-03 21:29:04 +02:00
|
|
|
#include "td/actor/actor.h"
|
2020-11-23 01:24:36 +01:00
|
|
|
#include "td/actor/ConcurrentScheduler.h"
|
2018-07-03 21:29:04 +02:00
|
|
|
|
2021-01-01 13:59:53 +01:00
|
|
|
#include "td/utils/algorithm.h"
|
2019-02-12 22:26:36 +01:00
|
|
|
#include "td/utils/common.h"
|
2018-12-31 20:04:05 +01:00
|
|
|
#include "td/utils/crypto.h"
|
2020-10-11 00:59:27 +02:00
|
|
|
#include "td/utils/ExitGuard.h"
|
2018-12-31 20:04:05 +01:00
|
|
|
#include "td/utils/logging.h"
|
2019-04-23 12:02:10 +02:00
|
|
|
#include "td/utils/misc.h"
|
2018-12-31 20:04:05 +01:00
|
|
|
#include "td/utils/MpscPollableQueue.h"
|
2020-07-29 15:49:35 +02:00
|
|
|
#include "td/utils/port/RwMutex.h"
|
2018-12-31 20:04:05 +01:00
|
|
|
#include "td/utils/port/thread.h"
|
2021-06-02 14:43:56 +02:00
|
|
|
#include "td/utils/Slice.h"
|
2018-12-31 20:04:05 +01:00
|
|
|
|
2019-04-23 12:02:10 +02:00
|
|
|
#include <algorithm>
|
2018-07-23 13:45:31 +02:00
|
|
|
#include <atomic>
|
2020-11-19 23:32:58 +01:00
|
|
|
#include <limits>
|
2019-03-21 23:42:41 +01:00
|
|
|
#include <memory>
|
|
|
|
#include <mutex>
|
2020-07-30 03:04:57 +02:00
|
|
|
#include <queue>
|
2019-03-21 23:42:41 +01:00
|
|
|
#include <unordered_map>
|
2020-11-19 23:32:58 +01:00
|
|
|
#include <unordered_set>
|
2018-12-31 20:04:05 +01:00
|
|
|
|
|
|
|
namespace td {
|
|
|
|
|
2020-07-29 15:49:35 +02:00
|
|
|
#if TD_THREAD_UNSUPPORTED || TD_EVENTFD_UNSUPPORTED
|
2020-07-30 03:04:57 +02:00
|
|
|
class TdReceiver {
|
2020-07-29 15:49:35 +02:00
|
|
|
public:
|
2020-10-05 13:32:23 +02:00
|
|
|
ClientManager::Response receive(double timeout) {
|
2020-10-12 19:18:48 +02:00
|
|
|
if (!responses_.empty()) {
|
2020-07-29 15:49:35 +02:00
|
|
|
auto result = std::move(responses_.front());
|
2020-08-25 15:32:22 +02:00
|
|
|
responses_.pop();
|
2020-07-29 15:49:35 +02:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
return {0, 0, nullptr};
|
|
|
|
}
|
2020-07-30 03:04:57 +02:00
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
unique_ptr<TdCallback> create_callback(ClientManager::ClientId client_id) {
|
2021-07-04 04:58:54 +02:00
|
|
|
class Callback final : public TdCallback {
|
2018-09-15 18:33:27 +02:00
|
|
|
public:
|
2020-10-05 13:32:23 +02:00
|
|
|
Callback(ClientManager::ClientId client_id, TdReceiver *impl) : client_id_(client_id), impl_(impl) {
|
2018-09-15 18:33:27 +02:00
|
|
|
}
|
2021-07-03 22:51:36 +02:00
|
|
|
void on_result(uint64 id, td_api::object_ptr<td_api::Object> result) final {
|
2020-10-12 19:18:48 +02:00
|
|
|
impl_->responses_.push({client_id_, id, std::move(result)});
|
2018-09-15 18:33:27 +02:00
|
|
|
}
|
2021-07-03 22:51:36 +02:00
|
|
|
void on_error(uint64 id, td_api::object_ptr<td_api::error> error) final {
|
2020-10-12 19:18:48 +02:00
|
|
|
impl_->responses_.push({client_id_, id, std::move(error)});
|
2018-09-15 18:33:27 +02:00
|
|
|
}
|
|
|
|
Callback(const Callback &) = delete;
|
|
|
|
Callback &operator=(const Callback &) = delete;
|
|
|
|
Callback(Callback &&) = delete;
|
|
|
|
Callback &operator=(Callback &&) = delete;
|
2021-07-03 22:51:36 +02:00
|
|
|
~Callback() final {
|
2020-10-12 19:18:48 +02:00
|
|
|
impl_->responses_.push({client_id_, 0, nullptr});
|
2018-09-15 18:33:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2020-10-05 13:32:23 +02:00
|
|
|
ClientManager::ClientId client_id_;
|
2020-07-30 03:04:57 +02:00
|
|
|
TdReceiver *impl_;
|
2018-09-15 18:33:27 +02:00
|
|
|
};
|
2020-07-29 15:49:35 +02:00
|
|
|
return td::make_unique<Callback>(client_id, this);
|
|
|
|
}
|
|
|
|
|
2020-10-05 22:05:16 +02:00
|
|
|
void add_response(ClientManager::ClientId client_id, uint64 id, td_api::object_ptr<td_api::Object> result) {
|
|
|
|
responses_.push({client_id, id, std::move(result)});
|
|
|
|
}
|
|
|
|
|
2020-07-29 15:49:35 +02:00
|
|
|
private:
|
2020-10-05 13:32:23 +02:00
|
|
|
std::queue<ClientManager::Response> responses_;
|
2020-07-29 15:49:35 +02:00
|
|
|
};
|
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
class ClientManager::Impl final {
|
2020-07-29 15:49:35 +02:00
|
|
|
public:
|
2020-11-14 23:13:11 +01:00
|
|
|
ClientId create_client_id() {
|
2020-11-11 23:19:30 +01:00
|
|
|
CHECK(client_id_ != std::numeric_limits<ClientId>::max());
|
2020-07-29 15:49:35 +02:00
|
|
|
auto client_id = ++client_id_;
|
2020-11-12 12:45:18 +01:00
|
|
|
pending_clients_.insert(client_id);
|
2020-07-29 15:49:35 +02:00
|
|
|
return client_id;
|
|
|
|
}
|
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
void send(ClientId client_id, RequestId request_id, td_api::object_ptr<td_api::Function> &&request) {
|
2020-11-12 12:45:18 +01:00
|
|
|
if (pending_clients_.erase(client_id) != 0) {
|
|
|
|
if (tds_.empty()) {
|
|
|
|
CHECK(concurrent_scheduler_ == nullptr);
|
|
|
|
CHECK(options_.net_query_stats == nullptr);
|
|
|
|
options_.net_query_stats = std::make_shared<NetQueryStats>();
|
|
|
|
concurrent_scheduler_ = make_unique<ConcurrentScheduler>();
|
|
|
|
concurrent_scheduler_->init(0);
|
|
|
|
concurrent_scheduler_->start();
|
|
|
|
}
|
|
|
|
tds_[client_id] =
|
|
|
|
concurrent_scheduler_->create_actor_unsafe<Td>(0, "Td", receiver_.create_callback(client_id), options_);
|
|
|
|
}
|
2020-10-08 00:28:24 +02:00
|
|
|
requests_.push_back({client_id, request_id, std::move(request)});
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
Response receive(double timeout) {
|
|
|
|
if (!requests_.empty()) {
|
2020-10-08 00:28:24 +02:00
|
|
|
for (size_t i = 0; i < requests_.size(); i++) {
|
|
|
|
auto &request = requests_[i];
|
2020-10-08 12:59:03 +02:00
|
|
|
if (request.client_id <= 0 || request.client_id > client_id_) {
|
2020-10-09 13:25:06 +02:00
|
|
|
receiver_.add_response(request.client_id, request.id,
|
|
|
|
td_api::make_object<td_api::error>(400, "Invalid TDLib instance specified"));
|
2020-10-08 00:28:24 +02:00
|
|
|
continue;
|
|
|
|
}
|
2020-10-08 12:59:03 +02:00
|
|
|
auto it = tds_.find(request.client_id);
|
|
|
|
if (it == tds_.end() || it->second.empty()) {
|
2020-10-09 13:25:06 +02:00
|
|
|
receiver_.add_response(request.client_id, request.id,
|
|
|
|
td_api::make_object<td_api::error>(500, "Request aborted"));
|
2020-10-08 00:28:24 +02:00
|
|
|
continue;
|
|
|
|
}
|
2020-10-09 14:39:30 +02:00
|
|
|
|
|
|
|
CHECK(concurrent_scheduler_ != nullptr);
|
|
|
|
auto guard = concurrent_scheduler_->get_main_guard();
|
2020-10-08 00:28:24 +02:00
|
|
|
send_closure_later(it->second, &Td::request, request.id, std::move(request.request));
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
|
|
|
requests_.clear();
|
|
|
|
}
|
|
|
|
|
2020-10-12 19:18:48 +02:00
|
|
|
auto response = receiver_.receive(0);
|
2020-10-09 14:39:30 +02:00
|
|
|
if (response.client_id == 0 && concurrent_scheduler_ != nullptr) {
|
2018-09-14 20:40:33 +02:00
|
|
|
concurrent_scheduler_->run_main(0);
|
2020-10-12 19:18:48 +02:00
|
|
|
response = receiver_.receive(0);
|
2018-09-18 15:43:16 +02:00
|
|
|
} else {
|
|
|
|
ConcurrentScheduler::emscripten_clear_main_timeout();
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
2020-10-08 00:28:24 +02:00
|
|
|
if (response.request_id == 0 && response.object != nullptr &&
|
2020-10-08 14:20:22 +02:00
|
|
|
response.object->get_id() == td_api::updateAuthorizationState::ID &&
|
|
|
|
static_cast<const td_api::updateAuthorizationState *>(response.object.get())->authorization_state_->get_id() ==
|
|
|
|
td_api::authorizationStateClosed::ID) {
|
2020-10-10 21:05:20 +02:00
|
|
|
CHECK(concurrent_scheduler_ != nullptr);
|
2020-10-08 15:14:01 +02:00
|
|
|
auto guard = concurrent_scheduler_->get_main_guard();
|
2020-10-08 00:28:24 +02:00
|
|
|
auto it = tds_.find(response.client_id);
|
|
|
|
CHECK(it != tds_.end());
|
|
|
|
it->second.reset();
|
2020-10-10 21:08:41 +02:00
|
|
|
|
|
|
|
response.client_id = 0;
|
|
|
|
response.object = nullptr;
|
2020-10-08 00:28:24 +02:00
|
|
|
}
|
2020-10-06 19:46:54 +02:00
|
|
|
if (response.object == nullptr && response.client_id != 0 && response.request_id == 0) {
|
2020-10-08 00:28:24 +02:00
|
|
|
auto it = tds_.find(response.client_id);
|
|
|
|
CHECK(it != tds_.end());
|
|
|
|
CHECK(it->second.empty());
|
|
|
|
tds_.erase(it);
|
2020-10-10 21:08:41 +02:00
|
|
|
|
|
|
|
response.object = td_api::make_object<td_api::updateAuthorizationState>(
|
|
|
|
td_api::make_object<td_api::authorizationStateClosed>());
|
2020-10-09 14:39:30 +02:00
|
|
|
|
|
|
|
if (tds_.empty()) {
|
|
|
|
CHECK(options_.net_query_stats.use_count() == 1);
|
|
|
|
CHECK(options_.net_query_stats->get_count() == 0);
|
|
|
|
options_.net_query_stats = nullptr;
|
|
|
|
concurrent_scheduler_->finish();
|
|
|
|
concurrent_scheduler_ = nullptr;
|
|
|
|
reset_to_empty(tds_);
|
|
|
|
}
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
2020-07-29 15:49:35 +02:00
|
|
|
return response;
|
|
|
|
}
|
|
|
|
|
2020-10-20 21:23:52 +02:00
|
|
|
Impl() = default;
|
2020-07-30 03:04:57 +02:00
|
|
|
Impl(const Impl &) = delete;
|
|
|
|
Impl &operator=(const Impl &) = delete;
|
|
|
|
Impl(Impl &&) = delete;
|
|
|
|
Impl &operator=(Impl &&) = delete;
|
2018-12-31 20:04:05 +01:00
|
|
|
~Impl() {
|
2020-10-09 14:39:30 +02:00
|
|
|
if (concurrent_scheduler_ == nullptr) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-12-31 20:04:05 +01:00
|
|
|
{
|
2018-09-18 15:43:16 +02:00
|
|
|
auto guard = concurrent_scheduler_->get_main_guard();
|
2020-07-29 15:49:35 +02:00
|
|
|
for (auto &td : tds_) {
|
2020-10-08 00:28:24 +02:00
|
|
|
td.second.reset();
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
2020-10-11 00:59:27 +02:00
|
|
|
while (!tds_.empty() && !ExitGuard::is_exited()) {
|
2020-10-12 19:18:48 +02:00
|
|
|
receive(0.1);
|
2020-10-11 20:21:38 +02:00
|
|
|
}
|
2020-11-22 21:30:40 +01:00
|
|
|
concurrent_scheduler_->finish();
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2020-10-09 13:25:06 +02:00
|
|
|
TdReceiver receiver_;
|
2020-07-29 15:49:35 +02:00
|
|
|
struct Request {
|
|
|
|
ClientId client_id;
|
|
|
|
RequestId id;
|
2020-10-05 13:32:23 +02:00
|
|
|
td_api::object_ptr<td_api::Function> request;
|
2020-07-29 15:49:35 +02:00
|
|
|
};
|
2020-10-08 00:28:24 +02:00
|
|
|
vector<Request> requests_;
|
2018-09-27 03:19:03 +02:00
|
|
|
unique_ptr<ConcurrentScheduler> concurrent_scheduler_;
|
2020-07-29 15:49:35 +02:00
|
|
|
ClientId client_id_{0};
|
2020-07-30 16:28:56 +02:00
|
|
|
Td::Options options_;
|
2020-11-12 12:45:18 +01:00
|
|
|
std::unordered_set<int32> pending_clients_;
|
2020-07-30 03:04:57 +02:00
|
|
|
std::unordered_map<int32, ActorOwn<Td>> tds_;
|
2020-07-29 15:49:35 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
class Client::Impl final {
|
|
|
|
public:
|
2020-11-14 23:13:11 +01:00
|
|
|
Impl() : client_id_(impl_.create_client_id()) {
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void send(Request request) {
|
2020-10-08 00:28:24 +02:00
|
|
|
impl_.send(client_id_, request.id, std::move(request.function));
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Response receive(double timeout) {
|
2020-10-12 19:18:48 +02:00
|
|
|
auto response = impl_.receive(timeout);
|
2020-08-31 15:04:53 +02:00
|
|
|
|
2020-07-29 15:49:35 +02:00
|
|
|
Response old_response;
|
2020-10-08 00:28:24 +02:00
|
|
|
old_response.id = response.request_id;
|
2020-07-29 15:49:35 +02:00
|
|
|
old_response.object = std::move(response.object);
|
|
|
|
return old_response;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2020-10-05 13:32:23 +02:00
|
|
|
ClientManager::Impl impl_;
|
|
|
|
ClientManager::ClientId client_id_;
|
2018-09-15 18:33:27 +02:00
|
|
|
};
|
2018-12-31 20:04:05 +01:00
|
|
|
|
2018-09-15 18:33:27 +02:00
|
|
|
#else
|
|
|
|
|
2021-07-04 04:58:54 +02:00
|
|
|
class MultiTd final : public Actor {
|
2020-10-06 19:47:22 +02:00
|
|
|
public:
|
|
|
|
explicit MultiTd(Td::Options options) : options_(std::move(options)) {
|
|
|
|
}
|
|
|
|
void create(int32 td_id, unique_ptr<TdCallback> callback) {
|
|
|
|
auto &td = tds_[td_id];
|
|
|
|
CHECK(td.empty());
|
|
|
|
|
|
|
|
string name = "Td";
|
2020-10-08 14:20:22 +02:00
|
|
|
auto context = std::make_shared<ActorContext>();
|
2020-10-06 19:47:22 +02:00
|
|
|
auto old_context = set_context(context);
|
|
|
|
auto old_tag = set_tag(to_string(td_id));
|
|
|
|
td = create_actor<Td>("Td", std::move(callback), options_);
|
|
|
|
set_context(old_context);
|
|
|
|
set_tag(old_tag);
|
|
|
|
}
|
|
|
|
|
|
|
|
void send(ClientManager::ClientId client_id, ClientManager::RequestId request_id,
|
|
|
|
td_api::object_ptr<td_api::Function> &&request) {
|
|
|
|
auto &td = tds_[client_id];
|
|
|
|
CHECK(!td.empty());
|
|
|
|
send_closure(td, &Td::request, request_id, std::move(request));
|
|
|
|
}
|
|
|
|
|
|
|
|
void close(int32 td_id) {
|
2020-10-08 22:04:40 +02:00
|
|
|
size_t erased_count = tds_.erase(td_id);
|
|
|
|
CHECK(erased_count > 0);
|
2020-10-06 19:47:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
Td::Options options_;
|
|
|
|
std::unordered_map<int32, ActorOwn<Td>> tds_;
|
|
|
|
};
|
|
|
|
|
2020-07-30 03:04:57 +02:00
|
|
|
class TdReceiver {
|
2019-03-21 10:59:20 +01:00
|
|
|
public:
|
2020-07-30 03:04:57 +02:00
|
|
|
TdReceiver() {
|
2020-10-12 19:18:48 +02:00
|
|
|
output_queue_ = std::make_shared<OutputQueue>();
|
|
|
|
output_queue_->init();
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
2020-07-30 03:04:57 +02:00
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
ClientManager::Response receive(double timeout) {
|
2020-07-30 16:38:36 +02:00
|
|
|
VLOG(td_requests) << "Begin to wait for updates with timeout " << timeout;
|
2020-10-12 19:18:48 +02:00
|
|
|
auto is_locked = receive_lock_.exchange(true);
|
|
|
|
if (is_locked) {
|
|
|
|
LOG(FATAL) << "Receive is called after Client destroy, or simultaneously from different threads";
|
2020-09-01 16:27:07 +02:00
|
|
|
}
|
2021-03-28 21:33:22 +02:00
|
|
|
auto response = receive_unlocked(clamp(timeout, 0.0, 1000000.0));
|
2020-10-12 19:18:48 +02:00
|
|
|
is_locked = receive_lock_.exchange(false);
|
|
|
|
CHECK(is_locked);
|
2020-10-05 13:32:23 +02:00
|
|
|
VLOG(td_requests) << "End to wait for updates, returning object " << response.request_id << ' '
|
|
|
|
<< response.object.get();
|
2020-07-30 16:38:36 +02:00
|
|
|
return response;
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
2020-07-30 03:04:57 +02:00
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
unique_ptr<TdCallback> create_callback(ClientManager::ClientId client_id) {
|
2021-07-04 04:58:54 +02:00
|
|
|
class Callback final : public TdCallback {
|
2019-04-23 10:56:32 +02:00
|
|
|
public:
|
2020-10-12 19:18:48 +02:00
|
|
|
explicit Callback(ClientManager::ClientId client_id, std::shared_ptr<OutputQueue> output_queue)
|
|
|
|
: client_id_(client_id), output_queue_(std::move(output_queue)) {
|
2019-04-23 10:56:32 +02:00
|
|
|
}
|
2021-07-03 22:51:36 +02:00
|
|
|
void on_result(uint64 id, td_api::object_ptr<td_api::Object> result) final {
|
2020-10-12 19:18:48 +02:00
|
|
|
output_queue_->writer_put({client_id_, id, std::move(result)});
|
2019-08-01 21:12:04 +02:00
|
|
|
}
|
2021-07-03 22:51:36 +02:00
|
|
|
void on_error(uint64 id, td_api::object_ptr<td_api::error> error) final {
|
2020-10-12 19:18:48 +02:00
|
|
|
output_queue_->writer_put({client_id_, id, std::move(error)});
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
|
|
|
Callback(const Callback &) = delete;
|
|
|
|
Callback &operator=(const Callback &) = delete;
|
|
|
|
Callback(Callback &&) = delete;
|
|
|
|
Callback &operator=(Callback &&) = delete;
|
2021-07-03 22:51:36 +02:00
|
|
|
~Callback() final {
|
2020-10-12 19:18:48 +02:00
|
|
|
output_queue_->writer_put({client_id_, 0, nullptr});
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2020-10-05 13:32:23 +02:00
|
|
|
ClientManager::ClientId client_id_;
|
2020-10-12 19:18:48 +02:00
|
|
|
std::shared_ptr<OutputQueue> output_queue_;
|
2019-04-23 10:56:32 +02:00
|
|
|
};
|
2020-10-12 19:18:48 +02:00
|
|
|
return td::make_unique<Callback>(client_id, output_queue_);
|
2019-03-21 10:59:20 +01:00
|
|
|
}
|
|
|
|
|
2020-10-05 22:05:16 +02:00
|
|
|
void add_response(ClientManager::ClientId client_id, uint64 id, td_api::object_ptr<td_api::Object> result) {
|
2020-10-12 19:18:48 +02:00
|
|
|
output_queue_->writer_put({client_id, id, std::move(result)});
|
2020-10-05 22:05:16 +02:00
|
|
|
}
|
|
|
|
|
2019-03-21 10:59:20 +01:00
|
|
|
private:
|
2020-10-05 13:32:23 +02:00
|
|
|
using OutputQueue = MpscPollableQueue<ClientManager::Response>;
|
2020-10-12 19:18:48 +02:00
|
|
|
std::shared_ptr<OutputQueue> output_queue_;
|
|
|
|
int output_queue_ready_cnt_{0};
|
|
|
|
std::atomic<bool> receive_lock_{false};
|
|
|
|
|
|
|
|
ClientManager::Response receive_unlocked(double timeout) {
|
|
|
|
if (output_queue_ready_cnt_ == 0) {
|
|
|
|
output_queue_ready_cnt_ = output_queue_->reader_wait_nonblock();
|
2020-07-30 16:38:36 +02:00
|
|
|
}
|
2020-10-12 19:18:48 +02:00
|
|
|
if (output_queue_ready_cnt_ > 0) {
|
|
|
|
output_queue_ready_cnt_--;
|
|
|
|
return output_queue_->reader_get_unsafe();
|
2020-07-30 16:38:36 +02:00
|
|
|
}
|
|
|
|
if (timeout != 0) {
|
2020-10-12 19:18:48 +02:00
|
|
|
output_queue_->reader_get_event_fd().wait(static_cast<int>(timeout * 1000));
|
|
|
|
return receive_unlocked(0);
|
2020-07-30 16:38:36 +02:00
|
|
|
}
|
|
|
|
return {0, 0, nullptr};
|
|
|
|
}
|
2019-03-21 10:59:20 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
class MultiImpl {
|
|
|
|
public:
|
2021-07-10 01:08:03 +02:00
|
|
|
static constexpr int32 ADDITIONAL_THREAD_COUNT = 3;
|
|
|
|
|
2020-07-30 21:59:23 +02:00
|
|
|
explicit MultiImpl(std::shared_ptr<NetQueryStats> net_query_stats) {
|
2019-03-21 10:59:20 +01:00
|
|
|
concurrent_scheduler_ = std::make_shared<ConcurrentScheduler>();
|
2021-07-10 01:08:03 +02:00
|
|
|
concurrent_scheduler_->init(ADDITIONAL_THREAD_COUNT);
|
2019-03-21 10:59:20 +01:00
|
|
|
concurrent_scheduler_->start();
|
|
|
|
|
|
|
|
{
|
|
|
|
auto guard = concurrent_scheduler_->get_main_guard();
|
2020-07-30 16:28:56 +02:00
|
|
|
Td::Options options;
|
|
|
|
options.net_query_stats = std::move(net_query_stats);
|
|
|
|
multi_td_ = create_actor<MultiTd>("MultiTd", std::move(options));
|
2019-03-21 10:59:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
scheduler_thread_ = thread([concurrent_scheduler = concurrent_scheduler_] {
|
|
|
|
while (concurrent_scheduler->run_main(10)) {
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2019-03-21 23:42:41 +01:00
|
|
|
MultiImpl(const MultiImpl &) = delete;
|
|
|
|
MultiImpl &operator=(const MultiImpl &) = delete;
|
|
|
|
MultiImpl(MultiImpl &&) = delete;
|
|
|
|
MultiImpl &operator=(MultiImpl &&) = delete;
|
|
|
|
|
2020-11-12 12:45:18 +01:00
|
|
|
static int32 create_id() {
|
|
|
|
auto result = current_id_.fetch_add(1);
|
|
|
|
CHECK(result <= static_cast<uint32>(std::numeric_limits<int32>::max()));
|
|
|
|
return static_cast<int32>(result);
|
|
|
|
}
|
|
|
|
|
|
|
|
void create(int32 td_id, unique_ptr<TdCallback> callback) {
|
|
|
|
auto guard = concurrent_scheduler_->get_send_guard();
|
|
|
|
send_closure(multi_td_, &MultiTd::create, td_id, std::move(callback));
|
2019-03-21 10:59:20 +01:00
|
|
|
}
|
2019-03-21 23:42:41 +01:00
|
|
|
|
2020-10-08 12:59:03 +02:00
|
|
|
static bool is_valid_client_id(int32 client_id) {
|
2020-11-11 23:19:30 +01:00
|
|
|
return client_id > 0 && static_cast<uint32>(client_id) < current_id_.load();
|
2020-10-08 12:59:03 +02:00
|
|
|
}
|
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
void send(ClientManager::ClientId client_id, ClientManager::RequestId request_id,
|
|
|
|
td_api::object_ptr<td_api::Function> &&request) {
|
2019-03-21 10:59:20 +01:00
|
|
|
auto guard = concurrent_scheduler_->get_send_guard();
|
2020-10-05 13:32:23 +02:00
|
|
|
send_closure(multi_td_, &MultiTd::send, client_id, request_id, std::move(request));
|
2019-03-21 10:59:20 +01:00
|
|
|
}
|
2019-03-21 23:42:41 +01:00
|
|
|
|
2020-10-06 19:46:54 +02:00
|
|
|
void close(ClientManager::ClientId client_id) {
|
2019-03-21 10:59:20 +01:00
|
|
|
auto guard = concurrent_scheduler_->get_send_guard();
|
2020-10-06 19:46:54 +02:00
|
|
|
send_closure(multi_td_, &MultiTd::close, client_id);
|
2019-03-21 10:59:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
~MultiImpl() {
|
|
|
|
{
|
|
|
|
auto guard = concurrent_scheduler_->get_send_guard();
|
|
|
|
multi_td_.reset();
|
|
|
|
Scheduler::instance()->finish();
|
|
|
|
}
|
2020-11-22 22:18:01 +01:00
|
|
|
if (!ExitGuard::is_exited()) {
|
|
|
|
scheduler_thread_.join();
|
|
|
|
} else {
|
|
|
|
scheduler_thread_.detach();
|
2020-10-11 20:21:38 +02:00
|
|
|
}
|
2020-11-22 22:18:01 +01:00
|
|
|
concurrent_scheduler_->finish();
|
2019-03-21 10:59:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
std::shared_ptr<ConcurrentScheduler> concurrent_scheduler_;
|
2019-03-21 23:42:41 +01:00
|
|
|
thread scheduler_thread_;
|
|
|
|
ActorOwn<MultiTd> multi_td_;
|
2020-07-29 15:49:35 +02:00
|
|
|
|
2020-11-11 23:19:30 +01:00
|
|
|
static std::atomic<uint32> current_id_;
|
2019-03-21 10:59:20 +01:00
|
|
|
};
|
2018-09-15 18:33:27 +02:00
|
|
|
|
2020-11-11 23:19:30 +01:00
|
|
|
std::atomic<uint32> MultiImpl::current_id_{1};
|
2020-10-08 12:59:03 +02:00
|
|
|
|
2020-07-29 15:49:35 +02:00
|
|
|
class MultiImplPool {
|
2018-09-15 18:33:27 +02:00
|
|
|
public:
|
2020-07-29 15:49:35 +02:00
|
|
|
std::shared_ptr<MultiImpl> get() {
|
|
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
2020-07-30 21:59:23 +02:00
|
|
|
if (impls_.empty()) {
|
2020-07-30 22:39:10 +02:00
|
|
|
init_openssl_threads();
|
|
|
|
|
2021-07-10 01:08:03 +02:00
|
|
|
impls_.resize(clamp(thread::hardware_concurrency(), 8u, 20u) * 5 / 4);
|
|
|
|
CHECK(impls_.size() * (1 + MultiImpl::ADDITIONAL_THREAD_COUNT + 1 /* IOCP */) < 128);
|
2020-10-09 14:39:30 +02:00
|
|
|
|
|
|
|
net_query_stats_ = std::make_shared<NetQueryStats>();
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
|
|
|
auto &impl = *std::min_element(impls_.begin(), impls_.end(),
|
|
|
|
[](auto &a, auto &b) { return a.lock().use_count() < b.lock().use_count(); });
|
2020-10-08 00:28:24 +02:00
|
|
|
auto result = impl.lock();
|
|
|
|
if (!result) {
|
|
|
|
result = std::make_shared<MultiImpl>(net_query_stats_);
|
|
|
|
impl = result;
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
2020-10-08 00:28:24 +02:00
|
|
|
return result;
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
2019-03-21 10:59:20 +01:00
|
|
|
|
2020-10-09 14:39:30 +02:00
|
|
|
void try_clear() {
|
|
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
|
|
if (impls_.empty()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto &impl : impls_) {
|
|
|
|
if (impl.lock().use_count() != 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
reset_to_empty(impls_);
|
|
|
|
|
|
|
|
CHECK(net_query_stats_.use_count() == 1);
|
|
|
|
CHECK(net_query_stats_->get_count() == 0);
|
|
|
|
net_query_stats_ = nullptr;
|
|
|
|
}
|
|
|
|
|
2020-07-29 15:49:35 +02:00
|
|
|
private:
|
|
|
|
std::mutex mutex_;
|
2020-07-30 03:04:57 +02:00
|
|
|
std::vector<std::weak_ptr<MultiImpl>> impls_;
|
2020-10-09 14:39:30 +02:00
|
|
|
std::shared_ptr<NetQueryStats> net_query_stats_;
|
2020-07-29 15:49:35 +02:00
|
|
|
};
|
2018-12-31 20:04:05 +01:00
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
class ClientManager::Impl final {
|
2020-07-29 15:49:35 +02:00
|
|
|
public:
|
2020-11-14 23:13:11 +01:00
|
|
|
ClientId create_client_id() {
|
2020-11-12 12:45:18 +01:00
|
|
|
auto client_id = MultiImpl::create_id();
|
2020-07-29 15:49:35 +02:00
|
|
|
{
|
|
|
|
auto lock = impls_mutex_.lock_write().move_as_ok();
|
2020-11-12 12:45:18 +01:00
|
|
|
impls_[client_id]; // create empty MultiImplInfo
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
|
|
|
return client_id;
|
|
|
|
}
|
2020-07-30 03:04:57 +02:00
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
void send(ClientId client_id, RequestId request_id, td_api::object_ptr<td_api::Function> &&request) {
|
2020-07-29 15:49:35 +02:00
|
|
|
auto lock = impls_mutex_.lock_read().move_as_ok();
|
2020-10-08 12:59:03 +02:00
|
|
|
if (!MultiImpl::is_valid_client_id(client_id)) {
|
2020-10-09 13:25:06 +02:00
|
|
|
receiver_.add_response(client_id, request_id,
|
|
|
|
td_api::make_object<td_api::error>(400, "Invalid TDLib instance specified"));
|
2020-10-05 22:05:16 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-11-12 12:45:18 +01:00
|
|
|
|
2020-10-08 12:59:03 +02:00
|
|
|
auto it = impls_.find(client_id);
|
2020-11-12 12:45:18 +01:00
|
|
|
if (it != impls_.end() && it->second.impl == nullptr) {
|
|
|
|
lock.reset();
|
|
|
|
|
|
|
|
auto write_lock = impls_mutex_.lock_write().move_as_ok();
|
|
|
|
it = impls_.find(client_id);
|
|
|
|
if (it != impls_.end() && it->second.impl == nullptr) {
|
|
|
|
it->second.impl = pool_.get();
|
|
|
|
it->second.impl->create(client_id, receiver_.create_callback(client_id));
|
|
|
|
}
|
|
|
|
write_lock.reset();
|
|
|
|
|
|
|
|
lock = impls_mutex_.lock_read().move_as_ok();
|
|
|
|
it = impls_.find(client_id);
|
|
|
|
}
|
2020-10-08 12:59:03 +02:00
|
|
|
if (it == impls_.end() || it->second.is_closed) {
|
2020-10-09 13:25:06 +02:00
|
|
|
receiver_.add_response(client_id, request_id, td_api::make_object<td_api::error>(500, "Request aborted"));
|
2020-10-08 00:28:24 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
it->second.impl->send(client_id, request_id, std::move(request));
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
2020-07-30 03:04:57 +02:00
|
|
|
|
2020-07-29 15:49:35 +02:00
|
|
|
Response receive(double timeout) {
|
2020-10-12 19:18:48 +02:00
|
|
|
auto response = receiver_.receive(timeout);
|
2020-10-08 00:28:24 +02:00
|
|
|
if (response.request_id == 0 && response.object != nullptr &&
|
2020-10-08 14:20:22 +02:00
|
|
|
response.object->get_id() == td_api::updateAuthorizationState::ID &&
|
|
|
|
static_cast<const td_api::updateAuthorizationState *>(response.object.get())->authorization_state_->get_id() ==
|
2021-03-12 12:31:51 +01:00
|
|
|
td_api::authorizationStateClosed::ID) {
|
2020-10-08 00:28:24 +02:00
|
|
|
auto lock = impls_mutex_.lock_write().move_as_ok();
|
|
|
|
close_impl(response.client_id);
|
2020-10-10 21:08:41 +02:00
|
|
|
|
|
|
|
response.client_id = 0;
|
|
|
|
response.object = nullptr;
|
2020-10-08 00:28:24 +02:00
|
|
|
}
|
2020-10-06 19:46:54 +02:00
|
|
|
if (response.object == nullptr && response.client_id != 0 && response.request_id == 0) {
|
2020-07-29 15:49:35 +02:00
|
|
|
auto lock = impls_mutex_.lock_write().move_as_ok();
|
2020-10-08 00:28:24 +02:00
|
|
|
auto it = impls_.find(response.client_id);
|
|
|
|
CHECK(it != impls_.end());
|
|
|
|
CHECK(it->second.is_closed);
|
|
|
|
impls_.erase(it);
|
2020-10-10 21:08:41 +02:00
|
|
|
|
|
|
|
response.object = td_api::make_object<td_api::updateAuthorizationState>(
|
|
|
|
td_api::make_object<td_api::authorizationStateClosed>());
|
2020-10-09 14:39:30 +02:00
|
|
|
|
|
|
|
if (impls_.empty()) {
|
|
|
|
reset_to_empty(impls_);
|
|
|
|
pool_.try_clear();
|
|
|
|
}
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
2020-10-06 19:46:54 +02:00
|
|
|
return response;
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
|
|
|
|
2020-10-08 00:28:24 +02:00
|
|
|
void close_impl(ClientId client_id) {
|
|
|
|
auto it = impls_.find(client_id);
|
|
|
|
CHECK(it != impls_.end());
|
|
|
|
if (!it->second.is_closed) {
|
|
|
|
it->second.is_closed = true;
|
2020-11-12 12:45:18 +01:00
|
|
|
if (it->second.impl == nullptr) {
|
|
|
|
receiver_.add_response(client_id, 0, nullptr);
|
|
|
|
} else {
|
|
|
|
it->second.impl->close(client_id);
|
|
|
|
}
|
2020-10-08 00:28:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-30 03:04:57 +02:00
|
|
|
Impl() = default;
|
|
|
|
Impl(const Impl &) = delete;
|
|
|
|
Impl &operator=(const Impl &) = delete;
|
|
|
|
Impl(Impl &&) = delete;
|
|
|
|
Impl &operator=(Impl &&) = delete;
|
2020-07-29 15:49:35 +02:00
|
|
|
~Impl() {
|
2020-10-11 20:21:38 +02:00
|
|
|
if (ExitGuard::is_exited()) {
|
|
|
|
return;
|
|
|
|
}
|
2020-07-29 15:49:35 +02:00
|
|
|
for (auto &it : impls_) {
|
2020-10-08 00:28:24 +02:00
|
|
|
close_impl(it.first);
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
2020-10-11 00:59:27 +02:00
|
|
|
while (!impls_.empty() && !ExitGuard::is_exited()) {
|
2020-10-12 19:18:48 +02:00
|
|
|
receive(0.1);
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
MultiImplPool pool_;
|
2020-07-30 03:04:57 +02:00
|
|
|
RwMutex impls_mutex_;
|
2020-10-08 00:28:24 +02:00
|
|
|
struct MultiImplInfo {
|
|
|
|
std::shared_ptr<MultiImpl> impl;
|
|
|
|
bool is_closed = false;
|
|
|
|
};
|
|
|
|
std::unordered_map<ClientId, MultiImplInfo> impls_;
|
2020-10-09 13:25:06 +02:00
|
|
|
TdReceiver receiver_;
|
2020-07-29 15:49:35 +02:00
|
|
|
};
|
2018-12-31 20:04:05 +01:00
|
|
|
|
2020-07-29 15:49:35 +02:00
|
|
|
class Client::Impl final {
|
|
|
|
public:
|
|
|
|
Impl() {
|
|
|
|
static MultiImplPool pool;
|
|
|
|
multi_impl_ = pool.get();
|
2020-11-12 12:45:18 +01:00
|
|
|
td_id_ = MultiImpl::create_id();
|
|
|
|
multi_impl_->create(td_id_, receiver_.create_callback(td_id_));
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
|
|
|
|
2020-10-08 00:28:24 +02:00
|
|
|
void send(Request request) {
|
2018-12-31 20:04:05 +01:00
|
|
|
if (request.id == 0 || request.function == nullptr) {
|
|
|
|
LOG(ERROR) << "Drop wrong request " << request.id;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-29 15:49:35 +02:00
|
|
|
multi_impl_->send(td_id_, request.id, std::move(request.function));
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
|
|
|
|
2020-10-08 00:28:24 +02:00
|
|
|
Response receive(double timeout) {
|
2020-10-12 19:18:48 +02:00
|
|
|
auto response = receiver_.receive(timeout);
|
2020-07-29 15:49:35 +02:00
|
|
|
|
2020-10-08 00:28:24 +02:00
|
|
|
Response old_response;
|
|
|
|
old_response.id = response.request_id;
|
|
|
|
old_response.object = std::move(response.object);
|
|
|
|
return old_response;
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
|
|
|
|
2018-07-09 01:36:44 +02:00
|
|
|
Impl(const Impl &) = delete;
|
|
|
|
Impl &operator=(const Impl &) = delete;
|
|
|
|
Impl(Impl &&) = delete;
|
|
|
|
Impl &operator=(Impl &&) = delete;
|
2018-12-31 20:04:05 +01:00
|
|
|
~Impl() {
|
2020-08-06 20:28:44 +02:00
|
|
|
multi_impl_->close(td_id_);
|
2020-10-11 00:59:27 +02:00
|
|
|
while (!ExitGuard::is_exited()) {
|
2020-10-12 19:18:48 +02:00
|
|
|
auto response = receiver_.receive(0.1);
|
2020-10-06 19:46:54 +02:00
|
|
|
if (response.object == nullptr && response.client_id != 0 && response.request_id == 0) {
|
|
|
|
break;
|
|
|
|
}
|
2019-03-21 10:59:20 +01:00
|
|
|
}
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2019-03-21 10:59:20 +01:00
|
|
|
std::shared_ptr<MultiImpl> multi_impl_;
|
2020-10-09 13:25:06 +02:00
|
|
|
TdReceiver receiver_;
|
2019-03-21 10:59:20 +01:00
|
|
|
|
|
|
|
int32 td_id_;
|
2018-12-31 20:04:05 +01:00
|
|
|
};
|
|
|
|
#endif
|
|
|
|
|
2018-09-27 03:19:03 +02:00
|
|
|
Client::Client() : impl_(std::make_unique<Impl>()) {
|
2018-12-31 20:04:05 +01:00
|
|
|
}
|
|
|
|
|
2018-03-11 21:49:38 +01:00
|
|
|
void Client::send(Request &&request) {
|
2018-12-31 20:04:05 +01:00
|
|
|
impl_->send(std::move(request));
|
|
|
|
}
|
|
|
|
|
|
|
|
Client::Response Client::receive(double timeout) {
|
|
|
|
return impl_->receive(timeout);
|
|
|
|
}
|
|
|
|
|
2018-03-11 21:49:38 +01:00
|
|
|
Client::Response Client::execute(Request &&request) {
|
2018-12-31 20:04:05 +01:00
|
|
|
Response response;
|
|
|
|
response.id = request.id;
|
|
|
|
response.object = Td::static_request(std::move(request.function));
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
|
|
|
|
Client::~Client() = default;
|
|
|
|
Client::Client(Client &&other) = default;
|
|
|
|
Client &Client::operator=(Client &&other) = default;
|
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
ClientManager::ClientManager() : impl_(std::make_unique<Impl>()) {
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
|
|
|
|
2020-11-14 23:13:11 +01:00
|
|
|
ClientManager::ClientId ClientManager::create_client_id() {
|
|
|
|
return impl_->create_client_id();
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
2020-07-30 03:04:57 +02:00
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
void ClientManager::send(ClientId client_id, RequestId request_id, td_api::object_ptr<td_api::Function> &&request) {
|
|
|
|
impl_->send(client_id, request_id, std::move(request));
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
2020-07-30 03:04:57 +02:00
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
ClientManager::Response ClientManager::receive(double timeout) {
|
2020-07-29 15:49:35 +02:00
|
|
|
return impl_->receive(timeout);
|
|
|
|
}
|
2020-07-30 03:04:57 +02:00
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
td_api::object_ptr<td_api::Object> ClientManager::execute(td_api::object_ptr<td_api::Function> &&request) {
|
|
|
|
return Td::static_request(std::move(request));
|
2020-07-29 15:49:35 +02:00
|
|
|
}
|
|
|
|
|
2021-05-18 03:35:36 +02:00
|
|
|
static std::atomic<ClientManager::LogMessageCallbackPtr> log_message_callback;
|
|
|
|
|
|
|
|
static void log_message_callback_wrapper(int verbosity_level, CSlice message) {
|
|
|
|
auto callback = log_message_callback.load(std::memory_order_relaxed);
|
|
|
|
if (callback != nullptr) {
|
|
|
|
callback(verbosity_level, message.c_str());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ClientManager::set_log_message_callback(int max_verbosity_level, LogMessageCallbackPtr callback) {
|
|
|
|
if (callback == nullptr) {
|
|
|
|
::td::set_log_message_callback(max_verbosity_level, nullptr);
|
|
|
|
log_message_callback = nullptr;
|
|
|
|
} else {
|
|
|
|
log_message_callback = callback;
|
|
|
|
::td::set_log_message_callback(max_verbosity_level, log_message_callback_wrapper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-05 13:32:23 +02:00
|
|
|
ClientManager::~ClientManager() = default;
|
|
|
|
ClientManager::ClientManager(ClientManager &&other) = default;
|
|
|
|
ClientManager &ClientManager::operator=(ClientManager &&other) = default;
|
2020-07-29 15:49:35 +02:00
|
|
|
|
2020-10-11 10:08:56 +02:00
|
|
|
ClientManager *ClientManager::get_manager_singleton() {
|
|
|
|
static ClientManager client_manager;
|
|
|
|
static ExitGuard exit_guard;
|
|
|
|
return &client_manager;
|
|
|
|
}
|
|
|
|
|
2018-12-31 20:04:05 +01:00
|
|
|
} // namespace td
|