diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index e80d965f..3ef4df56 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -1867,13 +1867,7 @@ void GuiApplication::Tick() { if (need_to_rejoin_ && now - last_rejoin_check_time_ >= 1s) { last_rejoin_check_time_ = now; - need_to_rejoin_ = false; - for (const auto& [_, props] : remote_sessions_) { - if (props && props->rejoin_) { - ConnectTo(props->remote_id_, props->remote_password_, - props->remember_password_); - } - } + HandlePendingRejoins(); } const bool was_reloading_recent_connections = reload_recent_connections_; @@ -3703,6 +3697,7 @@ void GuiApplication::CloseRemoteClosedTabs() { } void GuiApplication::CloseStreamTab(const std::string& remote_id) { + CancelPendingConnection(remote_id); std::shared_ptr props; { std::shared_lock lock(remote_sessions_mutex_); diff --git a/apps/desktop/src/gui/runtime/connection_runtime.cpp b/apps/desktop/src/gui/runtime/connection_runtime.cpp index 40296ae3..e747e312 100644 --- a/apps/desktop/src/gui/runtime/connection_runtime.cpp +++ b/apps/desktop/src/gui/runtime/connection_runtime.cpp @@ -51,7 +51,8 @@ void GuiRuntime::HandleConnectionStatusChange() { } last_presence_request_attempt_at_ = now; const auto message = - BuildDevicePresenceRequest(client_id_, recent_connection_ids_, true).dump(); + BuildDevicePresenceRequest(client_id_, recent_connection_ids_, true, {}, + gallery_presence_ids_).dump(); if (SendSignalMessage(peer_, message.data(), message.size()) == 0) { next_presence_refresh_at_ = now + kPresenceRefreshInterval; } else { @@ -59,6 +60,39 @@ void GuiRuntime::HandleConnectionStatusChange() { } } +void GuiRuntime::HandlePendingRejoins() { + need_to_rejoin_ = false; + std::vector> pending; + { + std::shared_lock lock(remote_sessions_mutex_); + for (const auto& [_, props] : remote_sessions_) { + if (props && props->rejoin_ && !props->closing_) + pending.push_back(props); + } + } + for (const auto& props : pending) { + // Password submission already entered the validating state. Let Join + // report its result even when this device is absent from the presence + // cache (for example, a newly added DeskHub device). A failed presence + // probe has no auth callback and would leave the prompt validating forever. + ConnectTo(props->remote_id_, props->remote_password_, + props->remember_password_, true, props->wall_managed_.load()); + } +} + +void GuiRuntime::CancelPendingConnection(const std::string& remote_id) { + pending_reconnects_.erase(remote_id); + if (auto props = FindRemoteSession(remote_id)) props->rejoin_ = false; + std::lock_guard lock(pending_presence_probe_mutex_); + if (pending_presence_remote_id_ != remote_id) return; + 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; +} + void GuiRuntime::HandlePendingPresenceProbe() { bool has_action = false; bool should_connect = false; @@ -211,7 +245,8 @@ int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id, } const auto s = BuildDevicePresenceRequest( - client_id_, recent_connection_ids_, false, remote_id) + client_id_, recent_connection_ids_, false, remote_id, + gallery_presence_ids_) .dump(); int ret = SendSignalMessage(peer_, s.data(), s.size()); @@ -418,7 +453,8 @@ void GuiRuntime::HandleSessionCleanup() { recent_connection_image_save_time_ = 0; } for (const auto& [id, request] : reconnects) { - ConnectTo(id, request.password.c_str(), request.remember_password); + ConnectTo(id, request.password.c_str(), request.remember_password, false, + request.background); } } @@ -487,23 +523,28 @@ std::shared_ptr GuiRuntime::FindRemoteSession( } int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password, - bool remember_password, bool bypass_presence_check) { + bool remember_password, bool bypass_presence_check, + bool background) { // Do not recreate a failed peer or send another probe during a server cooldown. if (const auto current = FindRemoteSession(remote_id); current && current->connection_auth_.Get().retry_after > 0) { - focused_remote_id_ = remote_id; - show_connection_status_window_ = true; + if (!background) focused_remote_id_ = remote_id; + if (!background) show_connection_status_window_ = true; current->rejoin_ = false; return 0; } if (session_cleanup_tasks_.count(remote_id)) { - pending_reconnects_[remote_id] = {password ? password : "", remember_password}; + pending_reconnects_[remote_id] = {password ? password : "", + remember_password, background}; 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); + // Wall requests, including deferred reconnects, must not replace the single + // interactive presence probe. Join reports availability for each session. + if (!background && !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_]; @@ -516,7 +557,7 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password, } LOG_INFO("Connect to [{}]", remote_id); - focused_remote_id_ = remote_id; + if (!background) focused_remote_id_ = remote_id; auto stale = FindRemoteSession(remote_id); bool exists = stale != nullptr; @@ -546,7 +587,7 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password, pending_reconnects_[remote_id] = { keep_stored_password ? std::string(stale->remote_password_) : std::string(password), - remember_password}; + remember_password, background}; CloseRemoteSession(stale); LOG_INFO("[{}] Reconnecting after terminal peer status {}", remote_id, static_cast(status)); @@ -566,6 +607,11 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password, auto props = remote_sessions_[remote_id]; props->local_id_ = "C-" + std::string(client_id_); props->remote_id_ = remote_id; + props->wall_managed_ = background; + props->tab_opened_ = !background; + props->control_mouse_ = !background; + props->enable_mouse_control_ = !background; + props->audio_capture_button_pressed_ = !background; memcpy(&props->params_, ¶ms_, sizeof(Params)); props->params_.user_id = props->local_id_.c_str(); props->peer_events_ = std::make_shared(*this); @@ -573,7 +619,7 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password, props->peer_ = CreatePeer(&props->params_); props->connection_status_.store(ConnectionStatus::Connecting); - show_connection_status_window_ = true; + if (!background) show_connection_status_window_ = true; if (!props->peer_) { LOG_INFO("Create peer [{}] instance failed", props->local_id_); @@ -612,7 +658,7 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password, auto props = FindRemoteSession(remote_id); if (!props || props->closing_) return -1; if (!props->connection_established_) { - show_connection_status_window_ = true; + if (!background) show_connection_status_window_ = true; props->remember_password_ = remember_password; if (password && *password && diff --git a/apps/desktop/src/gui/runtime/device_presence_cache.h b/apps/desktop/src/gui/runtime/device_presence_cache.h index ad4103f6..4c26dd9a 100644 --- a/apps/desktop/src/gui/runtime/device_presence_cache.h +++ b/apps/desktop/src/gui/runtime/device_presence_cache.h @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -33,12 +34,19 @@ public: } } - bool IsOnline(const std::string &device_id, - Clock::time_point now = Clock::now()) const { + std::optional GetOnline(const std::string &device_id, + Clock::time_point now = Clock::now()) const { std::lock_guard lock(mutex_); const auto it = cache_.find(device_id); - return signal_connected_ && it != cache_.end() && it->second.online && - now - it->second.updated_at < kMaxAge; + if (!signal_connected_ || it == cache_.end() || + now - it->second.updated_at >= kMaxAge) + return std::nullopt; + return it->second.online; + } + + bool IsOnline(const std::string &device_id, + Clock::time_point now = Clock::now()) const { + return GetOnline(device_id, now).value_or(false); } void Clear() { @@ -58,4 +66,4 @@ private: } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/apps/desktop/src/gui/runtime/device_presence_request.h b/apps/desktop/src/gui/runtime/device_presence_request.h index 2f0e2092..9bb5cf21 100644 --- a/apps/desktop/src/gui/runtime/device_presence_request.h +++ b/apps/desktop/src/gui/runtime/device_presence_request.h @@ -16,22 +16,25 @@ namespace crossdesk { inline nlohmann::json BuildDevicePresenceRequest( const std::string& client_id, const std::vector& recent_ids, - bool subscribe, const std::string& probe_id = {}) { + bool subscribe, const std::string& probe_id = {}, + const std::vector& additional_ids = {}) { std::vector devices; - for (const auto& id : recent_ids) { + const auto append = [&](const std::string& id) { // Recent connection keys contain Y/N plus display settings after the ID. const std::string pure_id = id.substr(0, id.find_first_of("YN")); if (!pure_id.empty() && std::find(devices.begin(), devices.end(), pure_id) == devices.end()) { devices.push_back(pure_id); } - } + }; + for (const auto& id : recent_ids) append(id); + for (const auto& id : additional_ids) append(id); if (!probe_id.empty() && std::find(devices.begin(), devices.end(), probe_id) == devices.end()) { devices.push_back(probe_id); } - // Include recent devices even in a one-shot probe: older servers ignore - // subscribe=false and replace their subscriptions with this device list. + // Keep recent and wall devices even in a one-shot probe: older servers + // ignore subscribe=false and replace subscriptions with this device list. return {{"type", "recent_connections_presence"}, {"user_id", client_id}, {"devices", devices}, @@ -40,4 +43,4 @@ inline nlohmann::json BuildDevicePresenceRequest( } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/apps/desktop/src/gui/runtime/gui_runtime.h b/apps/desktop/src/gui/runtime/gui_runtime.h index 9452b488..7f753632 100644 --- a/apps/desktop/src/gui/runtime/gui_runtime.h +++ b/apps/desktop/src/gui/runtime/gui_runtime.h @@ -51,13 +51,16 @@ class GuiRuntime : protected gui_detail::GuiState { int CreateConnectionPeer(); int ConnectTo(const std::string& remote_id, const char* password, - bool remember_password, bool bypass_presence_check = false); + bool remember_password, bool bypass_presence_check = false, + bool background = false); int RequestSingleDevicePresence(const std::string& remote_id, const char* password, bool remember_password); int ForceSignalReconnect(); void HandleRecentConnections(); void HandleConnectionStatusChange(); + void HandlePendingRejoins(); + void CancelPendingConnection(const std::string& remote_id); void HandlePendingPresenceProbe(); void HandlePresenceProbeTimeout(); bool HasActiveSession(); diff --git a/apps/desktop/src/gui/runtime/peer_event_handler.cpp b/apps/desktop/src/gui/runtime/peer_event_handler.cpp index d3b95c14..158bea86 100644 --- a/apps/desktop/src/gui/runtime/peer_event_handler.cpp +++ b/apps/desktop/src/gui/runtime/peer_event_handler.cpp @@ -268,7 +268,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, if (props) { runtime->is_client_mode_ = true; - runtime->show_connection_status_window_ = true; + if (!props->wall_managed_) runtime->show_connection_status_window_ = true; props->connection_status_.store(status); if (status != ConnectionStatus::Connecting && status != ConnectionStatus::Gathering) props->connection_auth_.Complete(); @@ -304,13 +304,13 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, FreeRemoteAction(remote_action); } - if (!runtime->need_to_create_stream_window_) { + if (!props->wall_managed_ && !runtime->need_to_create_stream_window_) { runtime->need_to_create_stream_window_ = true; } props->connection_established_ = true; props->was_connected_ = true; props->recent_connection_dirty_ = true; - runtime->start_keyboard_capturer_ = true; + if (!props->wall_managed_) runtime->start_keyboard_capturer_ = true; break; } case ConnectionStatus::Disconnected: diff --git a/apps/desktop/src/gui/runtime/remote_session.h b/apps/desktop/src/gui/runtime/remote_session.h index 78d907b2..467e04a0 100644 --- a/apps/desktop/src/gui/runtime/remote_session.h +++ b/apps/desktop/src/gui/runtime/remote_session.h @@ -71,6 +71,8 @@ struct FileTransferState { // Runtime state for one connected or connecting remote endpoint. It groups the // connection, media, input, window and transfer data that share one lifetime. struct RemoteSession { + // Wall membership, stream-window visibility and input ownership are separate. + std::atomic wall_managed_{false}; std::atomic preview_subscription_supported_{false}; std::atomic preview_ack_{0}; std::atomic preview_accepted_{false}; @@ -136,7 +138,7 @@ struct RemoteSession { std::shared_ptr video_latency_ = std::make_shared(); bool tab_selected_ = false; - bool tab_opened_ = true; + std::atomic tab_opened_{true}; std::string remote_host_name_; // Host metadata and dismissal state belong to this connection and are // protected by remote_version_mutex_. diff --git a/apps/desktop/src/gui/runtime/runtime_state.h b/apps/desktop/src/gui/runtime/runtime_state.h index 3f0dee20..a596c520 100644 --- a/apps/desktop/src/gui/runtime/runtime_state.h +++ b/apps/desktop/src/gui/runtime/runtime_state.h @@ -56,6 +56,7 @@ struct RecentConnectionsState { std::vector> recent_connections_; std::vector recent_connection_ids_; + std::vector gallery_presence_ids_; int recent_connection_image_width_ = 160; int recent_connection_image_height_ = 90; uint32_t recent_connection_image_save_time_ = 0; @@ -192,6 +193,7 @@ struct ConnectionState { struct PendingReconnect { std::string password; bool remember_password = false; + bool background = false; }; std::unordered_map pending_reconnects_;