diff --git a/src/ctl/users.cpp b/src/ctl/users.cpp index d995074..6a9826d 100644 --- a/src/ctl/users.cpp +++ b/src/ctl/users.cpp @@ -8,10 +8,8 @@ #include #include -#include #include #include -#include namespace tgfuse::ctl { @@ -63,10 +61,7 @@ int exec_users_list() { try { tg::TelegramClient client(client_config); - client.start().get_result(); - - // Wait for TDLib to initialise - std::this_thread::sleep_for(std::chrono::seconds(2)); + client.start().get_result(); // Now waits for TDLib initialization auto state = client.get_auth_state().get_result(); diff --git a/src/tg/client.cpp b/src/tg/client.cpp index d692f86..6621c5e 100644 --- a/src/tg/client.cpp +++ b/src/tg/client.cpp @@ -258,6 +258,11 @@ class TelegramClient::Impl { return; } + // Prepare the initialization promise/future pair + init_promise_ = std::promise(); + init_future_ = init_promise_.get_future(); + init_completed_ = false; + // Configure TDLib logging before creating the client configure_tdlib_logging(); @@ -591,24 +596,28 @@ class TelegramClient::Impl { case td_api::authorizationStateWaitPhoneNumber::ID: spdlog::info("Authorization: waiting for phone number"); auth_state_ = AuthState::WAIT_PHONE; + complete_initialization(); // TDLib is now ready to accept queries auth_cv_.notify_all(); break; case td_api::authorizationStateWaitCode::ID: spdlog::info("Authorization: waiting for code"); auth_state_ = AuthState::WAIT_CODE; + complete_initialization(); // TDLib is now ready to accept queries auth_cv_.notify_all(); break; case td_api::authorizationStateWaitPassword::ID: spdlog::info("Authorization: waiting for password"); auth_state_ = AuthState::WAIT_PASSWORD; + complete_initialization(); // TDLib is now ready to accept queries auth_cv_.notify_all(); break; case td_api::authorizationStateReady::ID: spdlog::info("Authorization: ready"); auth_state_ = AuthState::READY; + complete_initialization(); // TDLib is now ready to accept queries auth_cv_.notify_all(); break; @@ -642,6 +651,18 @@ class TelegramClient::Impl { } } + // Wait for TDLib initialization to complete + void wait_initialized(int timeout_ms = 30000) { + if (init_completed_) { + return; + } + + if (init_future_.wait_for(std::chrono::milliseconds(timeout_ms)) == std::future_status::timeout) { + throw TimeoutException("TDLib initialization timeout"); + } + init_future_.get(); // Propagate any exception + } + void send_phone_number(const std::string& phone) { send_query(td_api::make_object(phone, nullptr), [](auto response) { spdlog::debug("Phone number sent"); @@ -1228,6 +1249,17 @@ class TelegramClient::Impl { } private: + // Signal that TDLib initialization is complete + void complete_initialization() { + if (!init_completed_.exchange(true)) { + try { + init_promise_.set_value(); + } catch (const std::future_error&) { + // Promise already satisfied - ignore + } + } + } + void configure_tdlib_logging() { // Set log verbosity level td::ClientManager::execute(td_api::make_object(config_.log_verbosity)); @@ -1286,6 +1318,11 @@ class TelegramClient::Impl { mutable std::mutex auth_mutex_; std::condition_variable auth_cv_; + // Initialization synchronisation (for start() to wait until TDLib is ready) + std::promise init_promise_; + std::future init_future_; + std::atomic init_completed_{false}; + // Message callback std::function message_callback_; std::mutex message_callback_mutex_; @@ -1334,6 +1371,7 @@ TelegramClient::~TelegramClient() = default; Task TelegramClient::start() { impl_->start(); + impl_->wait_initialized(); // Block until TDLib is ready co_return; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 68448aa..1ed70b6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,6 +3,7 @@ add_executable(tg-fuse-tests tg/types_test.cpp tg/cache_test.cpp tg/async_test.cpp + tg/client_init_test.cpp tg/formatters_test.cpp tg/bustache_format_test.cpp ) diff --git a/tests/tg/client_init_test.cpp b/tests/tg/client_init_test.cpp new file mode 100644 index 0000000..2ccd95d --- /dev/null +++ b/tests/tg/client_init_test.cpp @@ -0,0 +1,319 @@ +#include "tg/async.hpp" +#include "tg/exceptions.hpp" + +#include + +#include +#include +#include +#include + +namespace tg { +namespace { + +// Mock initialization synchronizer that mirrors TelegramClient::Impl's init logic +// This allows testing the synchronization mechanism without actual TDLib dependencies +class MockInitSynchronizer { +public: + MockInitSynchronizer() : init_completed_(false) {} + + void prepare() { + init_promise_ = std::promise(); + init_future_ = init_promise_.get_future(); + init_completed_ = false; + } + + void wait_initialized(int timeout_ms = 5000) { + if (init_completed_) { + return; + } + + if (init_future_.wait_for(std::chrono::milliseconds(timeout_ms)) == std::future_status::timeout) { + throw TimeoutException("Initialization timeout"); + } + init_future_.get(); + } + + void complete_initialization() { + if (!init_completed_.exchange(true)) { + try { + init_promise_.set_value(); + } catch (const std::future_error&) { + // Promise already satisfied - ignore + } + } + } + + bool is_completed() const { return init_completed_; } + +private: + std::promise init_promise_; + std::future init_future_; + std::atomic init_completed_; +}; + +// Test that wait_initialized blocks until complete_initialization is called +TEST(ClientInitTest, WaitBlocksUntilComplete) { + MockInitSynchronizer sync; + sync.prepare(); + + std::atomic wait_finished{false}; + + // Start a thread that waits for initialization + std::thread waiter([&sync, &wait_finished]() { + sync.wait_initialized(); + wait_finished = true; + }); + + // Give the waiter time to start waiting + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + EXPECT_FALSE(wait_finished); + + // Complete initialization from another thread (simulating TDLib update thread) + sync.complete_initialization(); + + waiter.join(); + EXPECT_TRUE(wait_finished); + EXPECT_TRUE(sync.is_completed()); +} + +// Test that wait_initialized returns immediately if already completed +TEST(ClientInitTest, WaitReturnsImmediatelyIfAlreadyComplete) { + MockInitSynchronizer sync; + sync.prepare(); + + // Complete before waiting + sync.complete_initialization(); + + auto start = std::chrono::steady_clock::now(); + sync.wait_initialized(); + auto elapsed = std::chrono::steady_clock::now() - start; + + // Should return almost immediately (less than 10ms) + EXPECT_LT(elapsed, std::chrono::milliseconds(10)); +} + +// Test timeout when initialization never completes +TEST(ClientInitTest, TimeoutWhenNeverCompletes) { + MockInitSynchronizer sync; + sync.prepare(); + + EXPECT_THROW(sync.wait_initialized(100), TimeoutException); +} + +// Test that multiple calls to complete_initialization are safe +TEST(ClientInitTest, MultipleCompleteCallsAreSafe) { + MockInitSynchronizer sync; + sync.prepare(); + + // Call complete multiple times from different threads + std::vector threads; + for (int i = 0; i < 10; ++i) { + threads.emplace_back([&sync]() { sync.complete_initialization(); }); + } + + for (auto& t : threads) { + t.join(); + } + + EXPECT_TRUE(sync.is_completed()); + EXPECT_NO_THROW(sync.wait_initialized()); +} + +// Test wait and complete from different threads (the actual TelegramClient pattern) +// Note: std::future is not thread-safe for concurrent wait_for() calls, +// so only one thread should wait while another completes - exactly as in production +TEST(ClientInitTest, WaitAndCompleteFromDifferentThreads) { + MockInitSynchronizer sync; + sync.prepare(); + + std::atomic wait_started{false}; + std::atomic wait_finished{false}; + + // Single waiter thread (like the main thread calling start()) + std::thread waiter([&sync, &wait_started, &wait_finished]() { + wait_started = true; + sync.wait_initialized(); + wait_finished = true; + }); + + // Wait for waiter to start + while (!wait_started) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + EXPECT_FALSE(wait_finished); + + // Complete from another thread (like the TDLib update thread) + std::thread completer([&sync]() { sync.complete_initialization(); }); + + completer.join(); + waiter.join(); + + EXPECT_TRUE(wait_finished); +} + +// Test fast completion (before wait starts) +TEST(ClientInitTest, FastCompletion) { + MockInitSynchronizer sync; + sync.prepare(); + + // Complete immediately + sync.complete_initialization(); + + // Wait should return immediately + EXPECT_NO_THROW(sync.wait_initialized()); +} + +// Test that prepare() can be called again after completion (for restart scenarios) +TEST(ClientInitTest, CanReprepareAfterCompletion) { + MockInitSynchronizer sync; + + // First cycle + sync.prepare(); + sync.complete_initialization(); + sync.wait_initialized(); + EXPECT_TRUE(sync.is_completed()); + + // Second cycle (simulating client restart) + sync.prepare(); + EXPECT_FALSE(sync.is_completed()); + + std::thread completer([&sync]() { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + sync.complete_initialization(); + }); + + sync.wait_initialized(); + completer.join(); + + EXPECT_TRUE(sync.is_completed()); +} + +// Stress test with rapid prepare/complete cycles +TEST(ClientInitTest, StressTestRapidCycles) { + MockInitSynchronizer sync; + + for (int cycle = 0; cycle < 100; ++cycle) { + sync.prepare(); + + std::thread completer([&sync]() { sync.complete_initialization(); }); + + sync.wait_initialized(); + completer.join(); + + EXPECT_TRUE(sync.is_completed()); + } +} + +// Test simulating actual TDLib authorization state flow +TEST(ClientInitTest, SimulateTdLibAuthFlow) { + MockInitSynchronizer sync; + sync.prepare(); + + // Simulate TDLib update thread processing authorization states + std::thread tdlib_thread([&sync]() { + // Simulate: authorizationStateWaitTdlibParameters (no signal) + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // Simulate: setTdlibParameters response received + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // Simulate: authorizationStateWaitPhoneNumber or authorizationStateReady + // This is when we signal initialization complete + sync.complete_initialization(); + }); + + // Main thread waits for initialization + auto start = std::chrono::steady_clock::now(); + sync.wait_initialized(); + auto elapsed = std::chrono::steady_clock::now() - start; + + tdlib_thread.join(); + + // Should complete in roughly 20ms (the simulated TDLib delay) + EXPECT_GE(elapsed, std::chrono::milliseconds(15)); + EXPECT_LT(elapsed, std::chrono::milliseconds(500)); +} + +// Test using Task pattern (as used in TelegramClient::start()) +TEST(ClientInitTest, TaskPatternIntegration) { + MockInitSynchronizer sync; + + // This simulates TelegramClient::start() + auto start_task = [&sync]() -> Task { + sync.prepare(); + + // Simulate spawning update thread (in real code this happens before wait) + std::thread update_thread([&sync]() { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + sync.complete_initialization(); + }); + update_thread.detach(); + + // Wait for initialization + sync.wait_initialized(); + co_return; + }; + + auto task = start_task(); + EXPECT_NO_THROW(task.get_result()); + EXPECT_TRUE(sync.is_completed()); +} + +// Test exception propagation through the initialization mechanism +class MockInitSynchronizerWithException { +public: + void prepare() { + init_promise_ = std::promise(); + init_future_ = init_promise_.get_future(); + init_completed_ = false; + } + + void wait_initialized(int timeout_ms = 5000) { + if (init_completed_) { + if (exception_) { + std::rethrow_exception(exception_); + } + return; + } + + if (init_future_.wait_for(std::chrono::milliseconds(timeout_ms)) == std::future_status::timeout) { + throw TimeoutException("Initialization timeout"); + } + init_future_.get(); // This will rethrow if set_exception was called + } + + void complete_with_error(std::exception_ptr ex) { + if (!init_completed_.exchange(true)) { + exception_ = ex; + try { + init_promise_.set_exception(ex); + } catch (const std::future_error&) { + // Promise already satisfied - ignore + } + } + } + +private: + std::promise init_promise_; + std::future init_future_; + std::atomic init_completed_{false}; + std::exception_ptr exception_; +}; + +TEST(ClientInitTest, ExceptionPropagation) { + MockInitSynchronizerWithException sync; + sync.prepare(); + + std::thread error_thread([&sync]() { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + sync.complete_with_error(std::make_exception_ptr(std::runtime_error("TDLib init failed"))); + }); + + EXPECT_THROW(sync.wait_initialized(), std::runtime_error); + error_thread.join(); +} + +} // namespace +} // namespace tg