#include #include #include #include #include #include #include #include #include #include #include #include "localization.h" #include "platform.h" #include "platform/video_renderer.h" #include "rd_log.h" #include "runtime/device_presence_request.h" #include "runtime/gui_runtime.h" namespace crossdesk { namespace { constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5); constexpr auto kPresenceRefreshInterval = std::chrono::seconds(30); constexpr auto kPresenceRetryInterval = std::chrono::seconds(1); } // namespace bool GuiRuntime::HasActiveSession() { if (stream_window_inited_) return true; std::shared_lock lock(connection_status_mutex_); return std::any_of(connection_status_.begin(), connection_status_.end(), [](const auto& entry) { return entry.second == ConnectionStatus::Connecting || entry.second == ConnectionStatus::Connected; }); } void GuiRuntime::HandleConnectionStatusChange() { if (!signal_connected_ || !peer_) { return; } const auto now = std::chrono::steady_clock::now(); if (now - last_presence_request_attempt_at_ < kPresenceRetryInterval) { return; } // Consume the flag before sending so a concurrent reconnect cannot lose its // refresh request. Keep it pending while disconnected or after a send error. const bool requested = need_to_send_recent_connections_.exchange(false); if (!requested && now < next_presence_refresh_at_) { return; } last_presence_request_attempt_at_ = now; const auto message = BuildDevicePresenceRequest(client_id_, recent_connection_ids_, true).dump(); if (SendSignalMessage(peer_, message.data(), message.size()) == 0) { next_presence_refresh_at_ = now + kPresenceRefreshInterval; } else { need_to_send_recent_connections_ = true; } } void GuiRuntime::HandlePendingPresenceProbe() { bool has_action = false; bool should_connect = false; bool remember_password = false; std::string remote_id; std::string password; { std::lock_guard lock(pending_presence_probe_mutex_); if (!pending_presence_probe_ || !pending_presence_result_ready_) { return; } has_action = true; should_connect = pending_presence_online_; remote_id = pending_presence_remote_id_; password = pending_presence_password_; remember_password = pending_presence_remember_password_; 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 (!has_action) { return; } if (should_connect) { ConnectTo(remote_id, password.c_str(), remember_password, true); return; } offline_warning_text_ = localization::device_offline[localization_language_index_]; show_offline_warning_window_ = true; } void GuiRuntime::HandlePresenceProbeTimeout() { 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); } } void GuiRuntime::HandleServerControllerDisconnected( const std::string& remote_id, const char* reason) { keyboard_.ReleaseRemotePressedKeys(remote_id, reason); #ifdef _WIN32 devices_.ReleaseRemoteMouseButtons(); #endif { std::lock_guard lock(remote_pointer_input_mutex_); last_remote_pointer_input_time_.erase(remote_id); } bool has_connected_controller = false; std::string remaining_controller_id; { std::unique_lock lock(connection_status_mutex_); connection_status_.erase(remote_id); devices_.OnControllerConnectionsChanged(); connection_host_names_.erase(remote_id); privacy_sessions_.Disconnected(remote_id); for (const auto& [id, status] : connection_status_) { if (status != ConnectionStatus::Connected) { continue; } has_connected_controller = true; if (remaining_controller_id.empty()) { remaining_controller_id = id; } } } if (has_connected_controller) { remote_client_id_ = remaining_controller_id; return; } LOG_INFO("Last remote controller disconnected; recovering privacy, reason={}", reason ? reason : "unknown"); privacy_.Disconnected(); need_to_create_server_window_.store(false, std::memory_order_release); need_to_destroy_server_window_.store(true, std::memory_order_release); is_server_mode_ = false; #if defined(__linux__) && !defined(__APPLE__) if (IsWaylandSession()) { // Keep Wayland capture session warm to avoid black screen on subsequent // reconnects. start_screen_capturer_ = true; LOG_INFO( "Keeping Wayland screen capturer running after disconnect to " "preserve reconnect stability"); } else { start_screen_capturer_ = false; } #else start_screen_capturer_ = false; #endif devices_.StopSpeakerCapturer(); start_mouse_controller_ = false; start_keyboard_capturer_ = false; remote_client_id_.clear(); audio_capture_ = false; devices_.ResetToInitialDisplay(); } int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id, const char* password, bool remember_password) { if (!signal_connected_ || !peer_) { return -1; } { std::lock_guard lock(pending_presence_probe_mutex_); 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; } const auto s = BuildDevicePresenceRequest( client_id_, recent_connection_ids_, false, remote_id) .dump(); int ret = SendSignalMessage(peer_, s.data(), s.size()); if (ret != 0) { std::lock_guard lock(pending_presence_probe_mutex_); 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; } return ret; } void GuiRuntime::CloseRemoteSession(std::shared_ptr props) { if (!props || props->closing_.exchange(true)) return; if (props->peer_events_) props->peer_events_->Deactivate(); props->connection_established_ = false; props->streaming_ = false; props->connection_status_.store(ConnectionStatus::Closed); std::shared_ptr> frame_snapshot; int video_width = 0; int video_height = 0; { std::lock_guard lock(props->video_frame_mutex_); frame_snapshot = props->front_frame_; video_width = props->video_width_; video_height = props->video_height_; if ((!frame_snapshot || frame_snapshot->empty()) && props->thumbnail_frame_ && !props->thumbnail_frame_->empty()) { frame_snapshot = props->thumbnail_frame_; video_width = props->thumbnail_width_; video_height = props->thumbnail_height_; } } auto* native_renderer = video_renderer_.get(); if ((!frame_snapshot || frame_snapshot->empty()) && native_renderer) { auto native_snapshot = std::make_shared>(); if (native_renderer->CopyLatestNv12(props->remote_id_, native_snapshot.get(), &video_width, &video_height)) { frame_snapshot = std::move(native_snapshot); } } if (native_renderer) { native_renderer->DiscardStream(props->remote_id_); video_frame_dirty_.store(true, std::memory_order_release); } PeerPtr* peer = nullptr; { // Audio/clipboard senders hold a shared map lock while using this pointer. std::unique_lock lock(remote_sessions_mutex_); peer = std::exchange(props->peer_, nullptr); } const auto queued_at = std::chrono::steady_clock::now(); session_cleanup_tasks_[props->remote_id_] = session_cleanup_queue_.PostTask( [props, peer, frame_snapshot, video_width, video_height, thumbnail = thumbnail_, queued_at]() mutable { if (peer) { LOG_INFO("[{}] Background leave connection [{}]", props->local_id_, props->remote_id_); LeaveConnection(peer, props->remote_id_.c_str()); DestroyPeer(&peer); } if (thumbnail && frame_snapshot && !frame_snapshot->empty() && video_width > 0 && video_height > 0) { thumbnail->SaveToThumbnail( reinterpret_cast(frame_snapshot->data()), video_width, video_height, props->remote_id_, props->remote_host_name_, props->remember_password_ ? props->remote_password_ : ""); } const auto elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - queued_at).count(); LOG_INFO("[{}] Session cleanup completed in {} ms", props->remote_id_, elapsed); }); } void GuiRuntime::CloseServerController(const std::string& remote_id) { if (!peer_ || remote_id.empty() || controller_cleanup_tasks_.count(remote_id)) return; controller_cleanup_tasks_[remote_id] = session_cleanup_queue_.PostTask( [peer = peer_, remote_id] { LeaveConnection(peer, remote_id.c_str()); }); } void GuiRuntime::CloseConnectionPeer() { if (!peer_) return; peer_events_->Deactivate(); std::vector controllers; { std::shared_lock lock(connection_status_mutex_); for (const auto& [id, status] : connection_status_) controllers.push_back(id); } for (const auto& id : controllers) HandleServerControllerDisconnected(id, "peer_closed"); devices_.StopMouseController(); devices_.StopScreenCapturer(); devices_.StopSpeakerCapturer(); devices_.StopKeyboardCapturer(); signal_connected_ = false; device_presence_cache_.SetSignalConnected(false); signal_status_ = SignalStatus::SignalClosed; PeerPtr* peer; { std::unique_lock lock(remote_sessions_mutex_); peer = std::exchange(peer_, nullptr); } // The same serial queue also owns outstanding controller disconnects, so // these finish before the shared peer is destroyed. connection_peer_cleanup_ = session_cleanup_queue_.PostTask( [peer, handler = peer_events_, id = std::string(client_id_)]() mutable { LOG_INFO("[{}] Background connection peer cleanup", id); LeaveConnection(peer, id.c_str()); DestroyPeer(&peer); }); } void GuiRuntime::CloseAllRemoteSessions() { CloseConnectionPeer(); is_client_mode_ = false; pending_reconnects_.clear(); std::vector> sessions; { std::shared_lock lock(remote_sessions_mutex_); for (const auto& [id, props] : remote_sessions_) sessions.push_back(props); } for (const auto& props : sessions) CloseRemoteSession(props); { std::unique_lock lock(remote_sessions_mutex_); remote_sessions_.clear(); } } void GuiRuntime::HandleSessionCleanup() { auto completed = [](std::future& task) { if (!task.valid() || task.wait_for(std::chrono::seconds(0)) != std::future_status::ready) return false; try { task.get(); } catch (const std::exception& error) { LOG_ERROR("Connection cleanup failed: {}", error.what()); } return true; }; completed(connection_peer_cleanup_); for (auto it = controller_cleanup_tasks_.begin(); it != controller_cleanup_tasks_.end();) { if (completed(it->second)) it = controller_cleanup_tasks_.erase(it); else ++it; } std::vector> reconnects; for (auto it = session_cleanup_tasks_.begin(); it != session_cleanup_tasks_.end();) { if (it->second.wait_for(std::chrono::seconds(0)) != std::future_status::ready) { ++it; continue; } try { it->second.get(); } catch (const std::exception& error) { LOG_ERROR("[{}] Session cleanup failed: {}", it->first, error.what()); } if (auto pending = pending_reconnects_.find(it->first); pending != pending_reconnects_.end()) { reconnects.emplace_back(pending->first, std::move(pending->second)); pending_reconnects_.erase(pending); } it = session_cleanup_tasks_.erase(it); reload_recent_connections_ = true; recent_connection_image_save_time_ = 0; } for (const auto& [id, request] : reconnects) { ConnectTo(id, request.password.c_str(), request.remember_password); } } void GuiRuntime::WaitForSessionCleanup() { pending_reconnects_.clear(); auto drain = [](std::future& task) { if (!task.valid()) return; try { task.get(); } catch (const std::exception& error) { LOG_ERROR("Connection cleanup failed: {}", error.what()); } }; drain(connection_peer_cleanup_); for (auto& [id, task] : controller_cleanup_tasks_) drain(task); controller_cleanup_tasks_.clear(); for (auto& [id, task] : session_cleanup_tasks_) { try { task.get(); } catch (const std::exception& error) { LOG_ERROR("[{}] Session cleanup failed: {}", id, error.what()); } } session_cleanup_tasks_.clear(); } void GuiRuntime::ResetRemoteSessionResources( std::shared_ptr props) { props->net_traffic_stats_.Reset(); { std::lock_guard lock(props->video_frame_mutex_); props->video_latency_->Reset(); props->video_frame_timing_ = {}; props->front_frame_.reset(); props->back_frame_.reset(); props->thumbnail_frame_.reset(); props->thumbnail_width_ = 0; props->thumbnail_height_ = 0; props->background_snapshot_time_ = {}; props->video_width_ = 0; props->video_height_ = 0; props->video_size_ = 0; props->render_rect_dirty_ = true; props->stream_cleanup_pending_ = false; } { std::lock_guard lock(props->remote_cursor_state_mutex_); props->remote_cursor_state_ = {}; props->remote_cursor_state_received_ = false; props->cursor_presentation_ = {}; } } std::shared_ptr GuiRuntime::FindRemoteSession( const std::string& remote_id) { if (remote_id.empty()) { return nullptr; } std::shared_lock lock(remote_sessions_mutex_); auto it = remote_sessions_.find(remote_id); if (it == remote_sessions_.end()) { return nullptr; } return it->second; } int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password, bool remember_password, bool bypass_presence_check) { if (session_cleanup_tasks_.count(remote_id)) { pending_reconnects_[remote_id] = {password ? password : "", remember_password}; LOG_INFO("[{}] Reconnect queued until previous peer is destroyed", remote_id); return 0; } if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) { int ret = RequestSingleDevicePresence(remote_id, password, remember_password); if (ret != 0) { offline_warning_text_ = localization::device_offline[localization_language_index_]; show_offline_warning_window_ = true; LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret); } else { LOG_INFO("Presence probe requested for [{}] before connect", remote_id); } return -1; } LOG_INFO("Connect to [{}]", remote_id); focused_remote_id_ = remote_id; auto stale = FindRemoteSession(remote_id); bool exists = stale != nullptr; if (stale) { // A failed CreatePeer or a completed close can leave a session record // without a reusable peer. Recreate it instead of leaving the UI in // Connecting forever while JoinConnection has nothing to call. if (stale->peer_ == nullptr) { std::unique_lock lock(remote_sessions_mutex_); auto it = remote_sessions_.find(remote_id); if (it != remote_sessions_.end() && it->second == stale) { remote_sessions_.erase(it); exists = false; LOG_INFO("[{}] Recreating stale remote session before connect", remote_id); } } if (exists && !stale->closing_.load() && !stale->connection_established_) { const auto status = stale->connection_status_.load(); if (status == ConnectionStatus::Disconnected || status == ConnectionStatus::Failed || status == ConnectionStatus::Closed) { // The recreated session starts with an empty password; callers that // pass none expect the one stored by the previous attempt to be reused. const bool keep_stored_password = !password || !*password; pending_reconnects_[remote_id] = { keep_stored_password ? std::string(stale->remote_password_) : std::string(password), remember_password}; CloseRemoteSession(stale); LOG_INFO("[{}] Reconnecting after terminal peer status {}", remote_id, static_cast(status)); return 0; } } } if (!exists) { PeerPtr* peer_to_init = nullptr; std::string local_id; { std::unique_lock unique_lock(remote_sessions_mutex_); if (remote_sessions_.find(remote_id) == remote_sessions_.end()) { remote_sessions_[remote_id] = std::make_shared(); auto props = remote_sessions_[remote_id]; props->local_id_ = "C-" + std::string(client_id_); props->remote_id_ = remote_id; memcpy(&props->params_, ¶ms_, sizeof(Params)); props->params_.user_id = props->local_id_.c_str(); props->peer_events_ = std::make_shared(*this); props->params_.user_data = props->peer_events_.get(); props->peer_ = CreatePeer(&props->params_); props->connection_status_.store(ConnectionStatus::Connecting); show_connection_status_window_ = true; if (!props->peer_) { LOG_INFO("Create peer [{}] instance failed", props->local_id_); return -1; } const auto& displays = devices_.display_info_list(); for (size_t index = 0; index < displays.size(); ++index) { const std::string stream_id = MakeDisplayStreamId(index); AddVideoStream(props->peer_, stream_id.c_str()); } AddAudioStream(props->peer_, props->audio_label_.c_str()); AddDataStream(props->peer_, props->data_label_.c_str(), false); AddDataStream(props->peer_, props->mouse_label_.c_str(), true); AddDataStream(props->peer_, props->keyboard_label_.c_str(), true); AddDataStream(props->peer_, props->control_data_label_.c_str(), true); AddDataStream(props->peer_, props->file_label_.c_str(), true); AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true); AddDataStream(props->peer_, props->clipboard_label_.c_str(), true); props->connection_status_.store(ConnectionStatus::Connecting); peer_to_init = props->peer_; local_id = props->local_id_; } } if (peer_to_init) { LOG_INFO("[{}] Create peer instance successful", local_id); Init(peer_to_init); LOG_INFO("[{}] Peer init finish", local_id); } } int ret = -1; auto props = FindRemoteSession(remote_id); if (!props || props->closing_) return -1; if (!props->connection_established_) { props->connection_status_.store(ConnectionStatus::Connecting); show_connection_status_window_ = true; props->remember_password_ = remember_password; if (strcmp(password, "") != 0 && strcmp(password, props->remote_password_) != 0) { strncpy(props->remote_password_, password, sizeof(props->remote_password_) - 1); props->remote_password_[sizeof(props->remote_password_) - 1] = '\0'; } std::string remote_id_with_pwd = remote_id + "@" + password; if (props->peer_) { ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str()); if (0 == ret) { props->rejoin_ = false; } else { props->rejoin_ = true; need_to_rejoin_ = true; } } } return 0; } } // namespace crossdesk