From 5db309243b16376a8169b659acdca4d4577e789c Mon Sep 17 00:00:00 2001 From: dijunkun Date: Sun, 5 Jul 2026 00:52:21 +0800 Subject: [PATCH] [fix] fix stuck connecting state when remote device is offline, refs #57 --- src/gui/panels/remote_peer_panel.cpp | 5 + src/gui/render.cpp | 75 ++++++++++++++ src/gui/render.h | 4 + src/gui/render_callback.cpp | 7 +- src/gui/windows/connection_status_window.cpp | 15 ++- submodules/minirtc | 2 +- tests/connection_status_protocol_test.cpp | 100 +++++++++++++++++++ xmake/targets.lua | 5 + 8 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 tests/connection_status_protocol_test.cpp diff --git a/src/gui/panels/remote_peer_panel.cpp b/src/gui/panels/remote_peer_panel.cpp index ec5243e..bb4f68a 100644 --- a/src/gui/panels/remote_peer_panel.cpp +++ b/src/gui/panels/remote_peer_panel.cpp @@ -249,6 +249,11 @@ int Render::ConnectTo(const std::string& remote_id, const char* password, // std::shared_lock read_lock(client_properties_mutex_); auto props = client_properties_[remote_id]; if (!props->connection_established_) { + props->connection_status_.store(ConnectionStatus::Connecting); + props->connection_attempt_active_.store(true); + props->connection_attempt_started_at_ = std::chrono::steady_clock::now(); + show_connection_status_window_ = true; + props->remember_password_ = remember_password; if (strcmp(password, "") != 0 && strcmp(password, props->remote_password_) != 0) { diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 041ab53..94c1016 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -46,6 +46,13 @@ namespace crossdesk { namespace { constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500; +constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5); +constexpr auto kConnectionAttemptTimeout = std::chrono::seconds(20); + +bool IsConnectionAttemptPending(ConnectionStatus status) { + return status == ConnectionStatus::Connecting || + status == ConnectionStatus::Gathering; +} const ImWchar* GetMultilingualGlyphRanges() { static std::vector glyph_ranges; @@ -2078,6 +2085,7 @@ void Render::MainLoop() { HandleRecentConnections(); HandleConnectionStatusChange(); HandlePendingPresenceProbe(); + HandleConnectionTimeouts(); HandleStreamWindow(); HandleServerWindow(); HandleWindowsServiceIntegration(); @@ -2398,6 +2406,72 @@ void Render::HandlePendingPresenceProbe() { show_offline_warning_window_ = true; } +void Render::HandleConnectionTimeouts() { + const auto now = std::chrono::steady_clock::now(); + + bool presence_probe_timed_out = false; + std::string presence_remote_id; + { + std::lock_guard lock(pending_presence_probe_mutex_); + if (pending_presence_probe_ && !pending_presence_result_ready_ && + now - pending_presence_probe_started_at_ >= kPresenceProbeTimeout) { + presence_probe_timed_out = true; + presence_remote_id = pending_presence_remote_id_; + pending_presence_probe_ = false; + pending_presence_result_ready_ = false; + pending_presence_online_ = false; + pending_presence_remote_id_.clear(); + pending_presence_password_.clear(); + pending_presence_remember_password_ = false; + } + } + + if (presence_probe_timed_out) { + offline_warning_text_ = + localization::device_offline[localization_language_index_]; + show_offline_warning_window_ = true; + LOG_WARN("Presence probe timed out for [{}]", presence_remote_id); + } + + bool rejoin_state_changed = false; + for (auto& [_, props] : client_properties_) { + if (!props || !props->connection_attempt_active_.load()) { + continue; + } + + const ConnectionStatus status = props->connection_status_.load(); + if (!IsConnectionAttemptPending(status)) { + props->connection_attempt_active_.store(false); + continue; + } + + if (now - props->connection_attempt_started_at_ < + kConnectionAttemptTimeout) { + continue; + } + + LOG_WARN("Connection to [{}] timed out, status={}", props->remote_id_, + static_cast(status)); + props->connection_attempt_active_.store(false); + props->connection_established_ = false; + props->rejoin_ = false; + props->connection_status_.store(ConnectionStatus::Failed); + focused_remote_id_ = props->remote_id_; + show_connection_status_window_ = true; + rejoin_state_changed = true; + } + + if (rejoin_state_changed) { + need_to_rejoin_ = false; + for (const auto& [_, props] : client_properties_) { + if (props && props->rejoin_) { + need_to_rejoin_ = true; + break; + } + } + } +} + int Render::RequestSingleDevicePresence(const std::string& remote_id, const char* password, bool remember_password) { @@ -2410,6 +2484,7 @@ int Render::RequestSingleDevicePresence(const std::string& remote_id, pending_presence_probe_ = true; pending_presence_result_ready_ = false; pending_presence_online_ = false; + pending_presence_probe_started_at_ = std::chrono::steady_clock::now(); pending_presence_remote_id_ = remote_id; pending_presence_password_ = password ? password : ""; pending_presence_remember_password_ = remember_password; diff --git a/src/gui/render.h b/src/gui/render.h index d62eb29..fcefbf5 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -110,6 +110,8 @@ class Render { SignalStatus signal_status_ = SignalStatus::SignalClosed; bool connection_established_ = false; bool rejoin_ = false; + std::atomic connection_attempt_active_ = false; + std::chrono::steady_clock::time_point connection_attempt_started_at_; bool net_traffic_stats_button_pressed_ = false; bool enable_mouse_control_ = true; bool mouse_controller_is_started_ = false; @@ -220,6 +222,7 @@ class Render { void HandleRecentConnections(); void HandleConnectionStatusChange(); void HandlePendingPresenceProbe(); + void HandleConnectionTimeouts(); void HandleStreamWindow(); void HandleServerWindow(); void Cleanup(); @@ -833,6 +836,7 @@ class Render { bool pending_presence_probe_ = false; bool pending_presence_result_ready_ = false; bool pending_presence_online_ = false; + std::chrono::steady_clock::time_point pending_presence_probe_started_at_; std::string pending_presence_remote_id_ = ""; std::string pending_presence_password_ = ""; bool pending_presence_remember_password_ = false; diff --git a/src/gui/render_callback.cpp b/src/gui/render_callback.cpp index 0fe4610..5bcc383 100644 --- a/src/gui/render_callback.cpp +++ b/src/gui/render_callback.cpp @@ -1507,6 +1507,10 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id, render->is_client_mode_ = true; render->show_connection_status_window_ = true; props->connection_status_.store(status); + if (status != ConnectionStatus::Connecting && + status != ConnectionStatus::Gathering) { + props->connection_attempt_active_.store(false); + } switch (status) { case ConnectionStatus::Connected: { @@ -1608,7 +1612,8 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id, } break; } - case ConnectionStatus::NoSuchTransmissionId: { + case ConnectionStatus::NoSuchTransmissionId: + case ConnectionStatus::RemoteUnavailable: { if (render->connect_button_pressed_) { props->connection_established_ = false; render->connect_button_label_ = diff --git a/src/gui/windows/connection_status_window.cpp b/src/gui/windows/connection_status_window.cpp index 88d5f93..a55d5ab 100644 --- a/src/gui/windows/connection_status_window.cpp +++ b/src/gui/windows/connection_status_window.cpp @@ -180,6 +180,19 @@ bool Render::ConnectionStatusWindow( DestroyPeer(&props->peer_); ret_flag = true; } + } else if (ConnectionStatus::RemoteUnavailable == status) { + text = localization::device_offline[localization_language_index_]; + ImGui::SetCursorPosX(connection_status_window_width * 0.43f); + ImGui::SetCursorPosY(connection_status_window_height * 0.67f); + // ok + if (ImGui::Button(localization::ok[localization_language_index_].c_str()) || + ImGui::IsKeyPressed(ImGuiKey_Enter) || + ImGui::IsKeyPressed(ImGuiKey_Escape)) { + show_connection_status_window_ = false; + re_enter_remote_id_ = true; + DestroyPeer(&props->peer_); + ret_flag = true; + } } auto text_width = ImGui::CalcTextSize(text.c_str()).x; @@ -193,4 +206,4 @@ bool Render::ConnectionStatusWindow( return ret_flag; } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk diff --git a/submodules/minirtc b/submodules/minirtc index 0d9a9d6..9e5ce69 160000 --- a/submodules/minirtc +++ b/submodules/minirtc @@ -1 +1 @@ -Subproject commit 0d9a9d6a32adf11352e5d5fc35dd9de5725e8139 +Subproject commit 9e5ce698df5d141006a090698b18cb379064abb7 diff --git a/tests/connection_status_protocol_test.cpp b/tests/connection_status_protocol_test.cpp new file mode 100644 index 0000000..ebff554 --- /dev/null +++ b/tests/connection_status_protocol_test.cpp @@ -0,0 +1,100 @@ +#include +#include +#include +#include +#include + +namespace { + +std::filesystem::path FindRepoRoot() { + std::filesystem::path current = std::filesystem::current_path(); + while (!current.empty()) { + if (std::filesystem::exists(current / "xmake.lua") && + std::filesystem::exists(current / "submodules/minirtc/src/api/minirtc.h")) { + return current; + } + current = current.parent_path(); + } + return {}; +} + +std::string ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) { + return {}; + } + + std::ostringstream stream; + stream << file.rdbuf(); + return stream.str(); +} + +bool ExpectContains(const char* name, const std::string& value, + const std::string& expected) { + if (value.find(expected) != std::string::npos) { + return true; + } + + std::cerr << name << " missing expected text: " << expected << "\n"; + return false; +} + +bool ExpectNotContains(const char* name, const std::string& value, + const std::string& unexpected) { + if (value.find(unexpected) == std::string::npos) { + return true; + } + + std::cerr << name << " contains unexpected text: " << unexpected << "\n"; + return false; +} + +} // namespace + +int main() { + const std::filesystem::path repo_root = FindRepoRoot(); + if (repo_root.empty()) { + std::cerr << "failed to locate repository root\n"; + return 1; + } + + const std::string minirtc_api = + ReadFile(repo_root / "submodules/minirtc/src/api/minirtc.h"); + const std::string peer_connection = + ReadFile(repo_root / "submodules/minirtc/src/pc/peer_connection.cpp"); + const std::string connection_status_window = + ReadFile(repo_root / "src/gui/windows/connection_status_window.cpp"); + const std::string render_h = ReadFile(repo_root / "src/gui/render.h"); + const std::string render_cpp = ReadFile(repo_root / "src/gui/render.cpp"); + const std::string remote_peer_panel = + ReadFile(repo_root / "src/gui/panels/remote_peer_panel.cpp"); + + bool ok = true; + ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable"); + ok &= ExpectNotContains("minirtc.h", minirtc_api, "DeviceOffline"); + ok &= ExpectContains("peer_connection.cpp", peer_connection, + "\"Remote unavailable\""); + ok &= ExpectContains("peer_connection.cpp", peer_connection, + "ConnectionStatus::RemoteUnavailable"); + ok &= ExpectNotContains("peer_connection.cpp", peer_connection, + "\"Device offline\""); + ok &= ExpectNotContains("peer_connection.cpp", peer_connection, + "ConnectionStatus::DeviceOffline"); + ok &= ExpectContains("connection_status_window.cpp", + connection_status_window, + "localization::device_offline"); + ok &= ExpectContains("render.h", render_h, + "pending_presence_probe_started_at_"); + ok &= ExpectContains("render.h", render_h, + "connection_attempt_started_at_"); + ok &= ExpectContains("render.cpp", render_cpp, + "HandleConnectionTimeouts"); + ok &= ExpectContains("render.cpp", render_cpp, + "kPresenceProbeTimeout"); + ok &= ExpectContains("render.cpp", render_cpp, + "kConnectionAttemptTimeout"); + ok &= ExpectContains("remote_peer_panel.cpp", remote_peer_panel, + "connection_attempt_started_at_"); + + return ok ? 0 : 1; +} diff --git a/xmake/targets.lua b/xmake/targets.lua index 35319b3..bec8a4f 100644 --- a/xmake/targets.lua +++ b/xmake/targets.lua @@ -50,6 +50,11 @@ function setup_targets() add_includedirs("src/device_controller", "src/common") add_files("tests/keyboard_state_protocol_test.cpp") + target("connection_status_protocol_test") + set_kind("binary") + set_default(false) + add_files("tests/connection_status_protocol_test.cpp") + target("windows_manifest_resource_test") set_kind("binary") set_default(false)