| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/sync_sessions/synced_session_tracker.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/functional/callback.h" |
| #include "base/logging.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/sync/protocol/session_specifics.pb.h" |
| #include "components/sync/protocol/sync_enums.pb.h" |
| #include "components/sync_device_info/device_info_proto_enum_util.h" |
| #include "components/sync_sessions/sync_sessions_client.h" |
| |
| namespace sync_sessions { |
| |
| namespace { |
| |
| // Maximum time we allow a local tab stay unmapped (i.e. closed) but not freed |
| // due to data not having been committed yet. After that time, the data will |
| // be dropped. |
| constexpr base::TimeDelta kMaxUnmappedButUnsyncedLocalTabAge = |
| base::Minutes(10); |
| // This is a generous cap to avoid issues with situations like sync being in |
| // error state (e.g. auth error) during which many tabs could be opened and |
| // closed, and still the information would not be committed. |
| constexpr int kMaxUnmappedButUnsyncedLocalTabCount = 20; |
| |
| // Helper for iterating through all tabs within a window, and all navigations |
| // within a tab, to find if there's a valid syncable url. |
| bool ShouldSyncSessionWindow(SyncSessionsClient* sessions_client, |
| const sessions::SessionWindow& window) { |
| for (const auto& tab : window.tabs) { |
| for (const sessions::SerializedNavigationEntry& navigation : |
| tab->navigations) { |
| if (sessions_client->ShouldSyncURL(navigation.virtual_url())) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| // Presentable means |foreign_session| must have syncable content. |
| bool IsPresentable(SyncSessionsClient* sessions_client, |
| const SyncedSession& foreign_session) { |
| for (const auto& [window_id, window] : foreign_session.windows) { |
| if (ShouldSyncSessionWindow(sessions_client, window->wrapped_window)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Verify that tab and window IDs appear only once within a session. Intended to |
| // prevent http://crbug.com/360822 and crbug.com/803205. |
| bool IsValidSessionHeader(const sync_pb::SessionHeader& header) { |
| std::set<int> session_window_ids; |
| std::set<int> session_tab_ids; |
| for (const sync_pb::SessionWindow& window : header.window()) { |
| if (!session_window_ids.insert(window.window_id()).second) { |
| return false; |
| } |
| |
| for (int tab_id : window.tab()) { |
| if (!session_tab_ids.insert(tab_id).second) { |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| void PopulateSyncedSessionWindowFromSpecifics( |
| const std::string& session_tag, |
| const sync_pb::SessionWindow& specifics, |
| base::Time mtime, |
| SyncedSessionWindow* synced_session_window, |
| SyncedSessionTracker* tracker) { |
| sessions::SessionWindow* session_window = |
| &synced_session_window->wrapped_window; |
| |
| // The session window must be initially empty (reset via |
| // ResetSessionTracking()) to avoid leaving dangling pointers in |
| // |synced_tab_map|. |
| // TODO(crbug.com/41365570): replace with a DCHECK once PutTabInWindow() isn't |
| // crashing anymore. |
| CHECK(session_window->tabs.empty()); |
| |
| if (specifics.has_window_id()) { |
| session_window->window_id = |
| SessionID::FromSerializedValue(specifics.window_id()); |
| } |
| if (specifics.has_selected_tab_index()) { |
| session_window->selected_tab_index = specifics.selected_tab_index(); |
| } |
| synced_session_window->window_type = specifics.browser_type(); |
| if (specifics.has_browser_type()) { |
| if (specifics.browser_type() == |
| sync_pb::SyncEnums_BrowserType_TYPE_TABBED) { |
| session_window->type = sessions::SessionWindow::TYPE_NORMAL; |
| } else { |
| // Note: custom tabs are treated like popup windows on restore, as you can |
| // restore a custom tab on a platform that doesn't support them. |
| session_window->type = sessions::SessionWindow::TYPE_POPUP; |
| } |
| } |
| |
| session_window->timestamp = mtime; |
| |
| for (int i = 0; i < specifics.tab_size(); i++) { |
| SessionID tab_id = SessionID::FromSerializedValue(specifics.tab(i)); |
| tracker->PutTabInWindow(session_tag, session_window->window_id, tab_id); |
| } |
| } |
| |
| void PopulateSyncedSessionFromSpecifics( |
| const std::string& session_tag, |
| const sync_pb::SessionHeader& header_specifics, |
| base::Time mtime, |
| SyncedSession* synced_session, |
| SyncedSessionTracker* tracker) { |
| if (header_specifics.has_client_name()) { |
| synced_session->SetSessionName(header_specifics.client_name()); |
| } |
| if (header_specifics.has_session_start_time_unix_epoch_millis()) { |
| synced_session->SetStartTime(base::Time::FromMillisecondsSinceUnixEpoch( |
| header_specifics.session_start_time_unix_epoch_millis())); |
| } |
| |
| syncer::DeviceInfo::FormFactor device_form_factor = |
| syncer::ToDeviceInfoFormFactor(header_specifics.device_form_factor()); |
| // Old clients only populate the device type, so the form factor needs to be |
| // inferred. |
| if (device_form_factor == syncer::DeviceInfo::FormFactor::kUnknown) { |
| device_form_factor = |
| syncer::DeriveFormFactorFromDeviceType(header_specifics.device_type()); |
| } |
| if (device_form_factor != syncer::DeviceInfo::FormFactor::kUnknown) { |
| synced_session->SetDeviceTypeAndFormFactor(header_specifics.device_type(), |
| device_form_factor); |
| } |
| synced_session->SetModifiedTime( |
| std::max(mtime, synced_session->GetModifiedTime())); |
| |
| // Process all the windows and their tab information. |
| int num_windows = header_specifics.window_size(); |
| DVLOG(1) << "Populating " << session_tag << " with " << num_windows |
| << " windows."; |
| |
| for (int i = 0; i < num_windows; ++i) { |
| const sync_pb::SessionWindow& window_s = header_specifics.window(i); |
| SessionID window_id = SessionID::FromSerializedValue(window_s.window_id()); |
| bool success = tracker->PutWindowInSession(session_tag, window_id); |
| // Window ID duplicates are filtered out in IsValidSessionHeader(). |
| DCHECK(success) << "Duplicate window ID in session"; |
| PopulateSyncedSessionWindowFromSpecifics( |
| session_tag, window_s, synced_session->GetModifiedTime(), |
| synced_session->windows[window_id].get(), tracker); |
| } |
| } |
| |
| } // namespace |
| |
| SyncedSessionTracker::TrackedSession::TrackedSession() = default; |
| |
| SyncedSessionTracker::TrackedSession::~TrackedSession() = default; |
| |
| SyncedSessionTracker::SyncedSessionTracker(SyncSessionsClient* sessions_client) |
| : sessions_client_(sessions_client) {} |
| |
| SyncedSessionTracker::~SyncedSessionTracker() = default; |
| |
| void SyncedSessionTracker::InitLocalSession( |
| const std::string& local_session_tag, |
| const std::string& local_session_name, |
| sync_pb::SyncEnums::DeviceType local_device_type, |
| syncer::DeviceInfo::FormFactor local_device_form_factor) { |
| DCHECK(local_session_tag_.empty()); |
| DCHECK(!local_session_tag.empty()); |
| local_session_tag_ = local_session_tag; |
| |
| SyncedSession* local_session = GetSession(local_session_tag); |
| local_session->SetSessionName(local_session_name); |
| local_session->SetDeviceTypeAndFormFactor(local_device_type, |
| local_device_form_factor); |
| local_session->SetSessionTag(local_session_tag); |
| // Note: Do *not* call `SetStartTime()` here! `InitLocalSession()` gets called |
| // on every browser startup (if sessions sync is enabled), but the session |
| // start time should only be set when the session is initially created. |
| } |
| |
| void SyncedSessionTracker::SetLocalSessionStartTime( |
| base::Time local_session_start_time) { |
| SyncedSession* local_session = GetSession(local_session_tag_); |
| local_session->SetStartTime(local_session_start_time); |
| } |
| |
| const std::string& SyncedSessionTracker::GetLocalSessionTag() const { |
| return local_session_tag_; |
| } |
| |
| std::vector<raw_ptr<const SyncedSession, VectorExperimental>> |
| SyncedSessionTracker::LookupAllSessions(SessionLookup lookup) const { |
| return LookupSessions(lookup, /*exclude_local_session=*/false); |
| } |
| |
| std::vector<raw_ptr<const SyncedSession, VectorExperimental>> |
| SyncedSessionTracker::LookupAllForeignSessions(SessionLookup lookup) const { |
| return LookupSessions(lookup, /*exclude_local_session=*/true); |
| } |
| |
| std::vector<const sessions::SessionWindow*> |
| SyncedSessionTracker::LookupSessionWindows( |
| const std::string& session_tag) const { |
| std::vector<const sessions::SessionWindow*> windows; |
| |
| const TrackedSession* session = LookupTrackedSession(session_tag); |
| if (!session) { |
| return windows; // We have no record of this session. |
| } |
| |
| for (const auto& [window_id, window] : session->synced_session.windows) { |
| if (!window->wrapped_window.tabs.empty()) { |
| windows.push_back(&window->wrapped_window); |
| } |
| } |
| |
| return windows; |
| } |
| |
| const sessions::SessionTab* SyncedSessionTracker::LookupSessionTab( |
| const std::string& tag, |
| SessionID tab_id) const { |
| if (!tab_id.is_valid()) { |
| return nullptr; |
| } |
| |
| const TrackedSession* session = LookupTrackedSession(tag); |
| if (!session) { |
| return nullptr; // We have no record of this session. |
| } |
| |
| auto tab_iter = session->synced_tab_map.find(tab_id); |
| if (tab_iter == session->synced_tab_map.end()) { |
| return nullptr; // We have no record of this tab. |
| } |
| |
| return tab_iter->second; |
| } |
| |
| std::optional<sync_pb::SyncEnums::BrowserType> |
| SyncedSessionTracker::LookupWindowType(const std::string& session_tag, |
| SessionID window_id) const { |
| const TrackedSession* session = LookupTrackedSession(session_tag); |
| if (!session) { |
| return std::nullopt; |
| } |
| |
| auto window_iter = session->synced_window_map.find(window_id); |
| if (window_iter == session->synced_window_map.end()) { |
| return std::nullopt; // We have no record of this window. |
| } |
| |
| return window_iter->second->window_type; |
| } |
| |
| std::set<int> SyncedSessionTracker::LookupTabNodeIds( |
| const std::string& session_tag) const { |
| const TrackedSession* session = LookupTrackedSession(session_tag); |
| return session ? session->tab_node_pool.GetAllTabNodeIds() : std::set<int>(); |
| } |
| |
| const SyncedSession* SyncedSessionTracker::LookupLocalSession() const { |
| return LookupSession(local_session_tag_); |
| } |
| |
| const SyncedSession* SyncedSessionTracker::LookupSession( |
| const std::string& session_tag) const { |
| const TrackedSession* session = LookupTrackedSession(session_tag); |
| return session ? &session->synced_session : nullptr; |
| } |
| |
| SyncedSession* SyncedSessionTracker::GetSession( |
| const std::string& session_tag) { |
| return &GetTrackedSession(session_tag)->synced_session; |
| } |
| |
| void SyncedSessionTracker::DeleteForeignSession( |
| const std::string& session_tag) { |
| DCHECK_NE(local_session_tag_, session_tag); |
| |
| auto iter = session_map_.find(session_tag); |
| if (iter == session_map_.end()) { |
| return; |
| } |
| |
| // SyncedSession's destructor will trigger deletion of windows which will in |
| // turn trigger the deletion of tabs. This doesn't affect the convenience |
| // maps. |
| session_map_.erase(iter); |
| } |
| |
| void SyncedSessionTracker::ResetSessionTracking( |
| const std::string& session_tag) { |
| TrackedSession* session = GetTrackedSession(session_tag); |
| |
| for (auto& [window_id, window] : session->synced_session.windows) { |
| // First unmap the tabs in the window. |
| for (auto& tab : window->wrapped_window.tabs) { |
| SessionID tab_id = tab->tab_id; |
| session->unmapped_tabs[tab_id] = std::move(tab); |
| } |
| window->wrapped_window.tabs.clear(); |
| |
| // Then unmap the window itself. |
| session->unmapped_windows[window_id] = std::move(window); |
| } |
| session->synced_session.windows.clear(); |
| } |
| |
| void SyncedSessionTracker::DeleteForeignTab(const std::string& session_tag, |
| int tab_node_id) { |
| DCHECK_NE(local_session_tag_, session_tag); |
| TrackedSession* session = LookupTrackedSession(session_tag); |
| if (session) { |
| session->tab_node_pool.DeleteTabNode(tab_node_id); |
| } |
| } |
| |
| const SyncedSessionTracker::TrackedSession* |
| SyncedSessionTracker::LookupTrackedSession( |
| const std::string& session_tag) const { |
| auto it = session_map_.find(session_tag); |
| return it == session_map_.end() ? nullptr : &it->second; |
| } |
| |
| SyncedSessionTracker::TrackedSession* |
| SyncedSessionTracker::LookupTrackedSession(const std::string& session_tag) { |
| auto it = session_map_.find(session_tag); |
| return it == session_map_.end() ? nullptr : &it->second; |
| } |
| |
| SyncedSessionTracker::TrackedSession* SyncedSessionTracker::GetTrackedSession( |
| const std::string& session_tag) { |
| TrackedSession* session = LookupTrackedSession(session_tag); |
| if (session) { |
| return session; |
| } |
| |
| session = &session_map_[session_tag]; |
| DVLOG(1) << "Creating new session with tag " << session_tag << " at " |
| << session; |
| session->synced_session.SetSessionTag(session_tag); |
| return session; |
| } |
| |
| std::vector<raw_ptr<const SyncedSession, VectorExperimental>> |
| SyncedSessionTracker::LookupSessions(SessionLookup lookup, |
| bool exclude_local_session) const { |
| std::vector<raw_ptr<const SyncedSession, VectorExperimental>> sessions; |
| for (const auto& [session_tag, tracked_session] : session_map_) { |
| const SyncedSession& session = tracked_session.synced_session; |
| if (lookup == PRESENTABLE && !IsPresentable(sessions_client_, session)) { |
| continue; |
| } |
| // The comparison against |local_session_tag_| deals with the currently |
| // active sync session (cache GUID or legacy session tag) but in addition |
| // IsRecentLocalCacheGuid() is used to filter out older values of the |
| // local cache GUID. |
| if (exclude_local_session && |
| (session_tag == local_session_tag_ || |
| sessions_client_->IsRecentLocalCacheGuid(session_tag))) { |
| continue; |
| } |
| sessions.push_back(&session); |
| } |
| return sessions; |
| } |
| |
| void SyncedSessionTracker::CleanupSessionImpl( |
| const std::string& session_tag, |
| const base::RepeatingCallback<bool(int /*tab_node_id*/)>& |
| is_tab_node_unsynced_cb) { |
| TrackedSession* session = LookupTrackedSession(session_tag); |
| if (!session) { |
| return; |
| } |
| |
| for (const auto& [window_id, window] : session->unmapped_windows) { |
| session->synced_window_map.erase(window_id); |
| } |
| session->unmapped_windows.clear(); |
| |
| int num_unmapped_and_unsynced = 0; |
| auto tab_it = session->unmapped_tabs.begin(); |
| while (tab_it != session->unmapped_tabs.end()) { |
| SessionID tab_id = tab_it->first; |
| |
| if (session_tag == local_session_tag_) { |
| int tab_node_id = session->tab_node_pool.GetTabNodeIdFromTabId(tab_id); |
| const base::TimeDelta time_since_last_modified = |
| base::Time::Now() - tab_it->second->timestamp; |
| |
| if ((time_since_last_modified < kMaxUnmappedButUnsyncedLocalTabAge) && |
| num_unmapped_and_unsynced < kMaxUnmappedButUnsyncedLocalTabCount && |
| is_tab_node_unsynced_cb.Run(tab_node_id)) { |
| // Our caller has decided that this tab node cannot be reused at this |
| // point because there are pending changes to be committed that would |
| // otherwise be lost). Hence, it stays unmapped but we do not free the |
| // tab node for now (until future retries). |
| ++tab_it; |
| ++num_unmapped_and_unsynced; |
| continue; |
| } |
| |
| session->tab_node_pool.FreeTab(tab_id); |
| } |
| |
| session->synced_tab_map.erase(tab_id); |
| tab_it = session->unmapped_tabs.erase(tab_it); |
| } |
| } |
| |
| bool SyncedSessionTracker::IsTabUnmappedForTesting(SessionID tab_id) { |
| const TrackedSession* session = LookupTrackedSession(local_session_tag_); |
| if (!session) { |
| return false; |
| } |
| |
| return session->unmapped_tabs.count(tab_id) != 0; |
| } |
| |
| bool SyncedSessionTracker::PutWindowInSession(const std::string& session_tag, |
| SessionID window_id) { |
| TrackedSession* session = GetTrackedSession(session_tag); |
| if (session->synced_session.windows.count(window_id) != 0) { |
| DVLOG(1) << "Window " << window_id << " already added to session " |
| << session_tag; |
| return false; |
| } |
| std::unique_ptr<SyncedSessionWindow> window; |
| |
| auto iter = session->unmapped_windows.find(window_id); |
| if (iter != session->unmapped_windows.end()) { |
| DCHECK_EQ(session->synced_window_map[window_id], iter->second.get()); |
| window = std::move(iter->second); |
| session->unmapped_windows.erase(iter); |
| DVLOG(1) << "Putting seen window " << window_id << " at " << window.get() |
| << "in " |
| << (session_tag == local_session_tag_ ? "local session" |
| : session_tag); |
| } else { |
| // Create the window. |
| window = std::make_unique<SyncedSessionWindow>(); |
| window->wrapped_window.window_id = window_id; |
| session->synced_window_map[window_id] = window.get(); |
| DVLOG(1) << "Putting new window " << window_id << " at " << window.get() |
| << "in " |
| << (session_tag == local_session_tag_ ? "local session" |
| : session_tag); |
| } |
| DCHECK_EQ(window->wrapped_window.window_id, window_id); |
| DCHECK(GetSession(session_tag)->windows.end() == |
| GetSession(session_tag)->windows.find(window_id)); |
| GetSession(session_tag)->windows[window_id] = std::move(window); |
| return true; |
| } |
| |
| void SyncedSessionTracker::PutTabInWindow(const std::string& session_tag, |
| SessionID window_id, |
| SessionID tab_id) { |
| TrackedSession* session = LookupTrackedSession(session_tag); |
| DCHECK(session); |
| |
| // We're called here for two reasons. 1) We've received an update to the |
| // SessionWindow information of a SessionHeader node for a session, |
| // and 2) The SessionHeader node for our local session changed. In both cases |
| // we need to update our tracking state to reflect the change. |
| // |
| // Because the SessionHeader nodes are separate from the individual tab nodes |
| // and we don't store tab_node_ids in the header / SessionWindow specifics, |
| // the tab_node_ids are not always available when processing headers. We know |
| // that we will eventually process (via GetTab) every single tab node in the |
| // system, so we permit ourselves to just call GetTab and ignore the result, |
| // creating a placeholder SessionTab in the process. |
| sessions::SessionTab* tab_ptr = GetTab(session_tag, tab_id); |
| |
| // The tab should be unmapped. |
| std::unique_ptr<sessions::SessionTab> tab; |
| auto it = session->unmapped_tabs.find(tab_id); |
| if (it != session->unmapped_tabs.end()) { |
| tab = std::move(it->second); |
| session->unmapped_tabs.erase(it); |
| } else { |
| // The tab has already been mapped, possibly because of the tab node id |
| // being reused across tabs. Find the existing tab and move it to the right |
| // window. |
| for (auto& [existing_window_id, existing_window] : |
| GetSession(session_tag)->windows) { |
| auto existing_tab_iter = |
| std::ranges::find(existing_window->wrapped_window.tabs, tab_ptr, |
| &std::unique_ptr<sessions::SessionTab>::get); |
| if (existing_tab_iter != existing_window->wrapped_window.tabs.end()) { |
| tab = std::move(*existing_tab_iter); |
| existing_window->wrapped_window.tabs.erase(existing_tab_iter); |
| |
| DVLOG(1) << "Moving tab " << tab_id << " from window " |
| << existing_window_id << " to " << window_id; |
| break; |
| } |
| } |
| // TODO(crbug.com/41365570): replace with a DCHECK once PutTabInWindow() |
| // isn't crashing anymore. |
| CHECK(tab) << " Unable to find tab " << tab_id |
| << " within unmapped tabs or previously mapped windows." |
| << " https://crbug.com/803205"; |
| } |
| |
| tab->window_id = window_id; |
| DVLOG(1) << " - tab " << tab_id << " added to window " << window_id; |
| DCHECK(GetSession(session_tag)->windows.find(window_id) != |
| GetSession(session_tag)->windows.end()); |
| GetSession(session_tag) |
| ->windows[window_id] |
| ->wrapped_window.tabs.push_back(std::move(tab)); |
| } |
| |
| void SyncedSessionTracker::OnTabNodeSeen(const std::string& session_tag, |
| int tab_node_id, |
| SessionID tab_id) { |
| // TODO(mastiz): Revisit if the exception for |local_session_tag_| can be |
| // avoided and treat all sessions equally, which ideally involves merging this |
| // function with ReassociateLocalTab(). |
| if (session_tag != local_session_tag_ && |
| tab_node_id != TabNodePool::kInvalidTabNodeID) { |
| GetTrackedSession(session_tag) |
| ->tab_node_pool.ReassociateTabNode(tab_node_id, tab_id); |
| } |
| } |
| |
| sessions::SessionTab* SyncedSessionTracker::GetTab( |
| const std::string& session_tag, |
| SessionID tab_id) { |
| // TODO(crbug.com/41365570): replace with a DCHECK once PutTabInWindow() isn't |
| // crashing anymore. |
| CHECK(tab_id.is_valid()); |
| |
| TrackedSession* session = GetTrackedSession(session_tag); |
| sessions::SessionTab* tab_ptr = nullptr; |
| auto iter = session->synced_tab_map.find(tab_id); |
| if (iter != session->synced_tab_map.end()) { |
| tab_ptr = iter->second; |
| |
| if (VLOG_IS_ON(1)) { |
| std::string title; |
| if (tab_ptr->navigations.size() > 0) { |
| title = |
| " (" + base::UTF16ToUTF8(tab_ptr->navigations.back().title()) + ")"; |
| } |
| DVLOG(1) << "Getting " |
| << (session_tag == local_session_tag_ ? "local session" |
| : session_tag) |
| << "'s seen tab " << tab_id << " at " << tab_ptr << " " << title; |
| } |
| } else { |
| auto tab = std::make_unique<sessions::SessionTab>(); |
| tab_ptr = tab.get(); |
| tab->tab_id = tab_id; |
| session->synced_tab_map[tab_id] = tab_ptr; |
| session->unmapped_tabs[tab_id] = std::move(tab); |
| DVLOG(1) << "Getting " |
| << (session_tag == local_session_tag_ ? "local session" |
| : session_tag) |
| << "'s new tab " << tab_id << " at " << tab_ptr; |
| } |
| DCHECK(tab_ptr); |
| DCHECK_EQ(tab_ptr->tab_id, tab_id); |
| return tab_ptr; |
| } |
| |
| void SyncedSessionTracker::CleanupSession(const std::string& session_tag) { |
| DCHECK_NE(session_tag, local_session_tag_); |
| // |is_tab_node_unsynced_cb| is only used for the local session, not needed |
| // here. |
| CleanupSessionImpl( |
| session_tag, |
| /*=is_tab_node_unsynced_cb=*/base::RepeatingCallback<bool(int)>()); |
| } |
| |
| std::set<int> SyncedSessionTracker::CleanupLocalTabs( |
| const base::RepeatingCallback<bool(int /*tab_node_id*/)>& |
| is_tab_node_unsynced_cb) { |
| DCHECK(!local_session_tag_.empty()); |
| TrackedSession* session = GetTrackedSession(local_session_tag_); |
| CleanupSessionImpl(local_session_tag_, is_tab_node_unsynced_cb); |
| return session->tab_node_pool.CleanupFreeTabNodes(); |
| } |
| |
| int SyncedSessionTracker::LookupTabNodeFromTabId(const std::string& session_tag, |
| SessionID tab_id) const { |
| const TrackedSession* session = LookupTrackedSession(session_tag); |
| DCHECK(session); |
| return session->tab_node_pool.GetTabNodeIdFromTabId(tab_id); |
| } |
| |
| SessionID SyncedSessionTracker::LookupTabIdFromTabNodeId( |
| const std::string& session_tag, |
| int tab_node_id) const { |
| const TrackedSession* session = LookupTrackedSession(session_tag); |
| DCHECK(session); |
| return session->tab_node_pool.GetTabIdFromTabNodeId(tab_node_id); |
| } |
| |
| int SyncedSessionTracker::AssociateLocalTabWithFreeTabNode(SessionID tab_id) { |
| DCHECK(!local_session_tag_.empty()); |
| TrackedSession* session = GetTrackedSession(local_session_tag_); |
| |
| // Ensure a placeholder SessionTab is in place, if not already. Although we |
| // don't need a SessionTab to fulfill this request, this forces the creation |
| // of one if it doesn't already exist. This helps to make sure we're tracking |
| // this |tab_id| if TabNodePool is, and everyone's data structures are kept in |
| // sync and as consistent as possible. |
| GetTab(local_session_tag_, tab_id); // Ignore result. |
| |
| return session->tab_node_pool.AssociateWithFreeTabNode(tab_id); |
| } |
| |
| void SyncedSessionTracker::ReassociateLocalTab(int tab_node_id, |
| SessionID new_tab_id) { |
| DCHECK(!local_session_tag_.empty()); |
| DCHECK_NE(TabNodePool::kInvalidTabNodeID, tab_node_id); |
| DCHECK(new_tab_id.is_valid()); |
| |
| TrackedSession* session = LookupTrackedSession(local_session_tag_); |
| DCHECK(session); |
| |
| SessionID old_tab_id = |
| session->tab_node_pool.GetTabIdFromTabNodeId(tab_node_id); |
| if (new_tab_id == old_tab_id) { |
| return; |
| } |
| |
| session->tab_node_pool.ReassociateTabNode(tab_node_id, new_tab_id); |
| |
| sessions::SessionTab* tab_ptr = nullptr; |
| |
| auto new_tab_iter = session->synced_tab_map.find(new_tab_id); |
| auto old_tab_iter = session->synced_tab_map.find(old_tab_id); |
| if (old_tab_id.is_valid() && old_tab_iter != session->synced_tab_map.end()) { |
| tab_ptr = old_tab_iter->second; |
| DCHECK(tab_ptr); |
| |
| // Remove the tab from the synced tab map under the old id. |
| session->synced_tab_map.erase(old_tab_iter); |
| |
| if (new_tab_iter != session->synced_tab_map.end()) { |
| // If both the old and the new tab already exist, delete the new tab |
| // and use the old tab in its place. |
| auto unmapped_tabs_iter = session->unmapped_tabs.find(new_tab_id); |
| if (unmapped_tabs_iter != session->unmapped_tabs.end()) { |
| session->unmapped_tabs.erase(unmapped_tabs_iter); |
| } else { |
| sessions::SessionTab* new_tab_ptr = new_tab_iter->second; |
| for (auto& [existing_window_id, existing_window] : |
| session->synced_session.windows) { |
| auto& existing_window_tabs = existing_window->wrapped_window.tabs; |
| auto tab_iter = |
| std::ranges::find(existing_window_tabs, new_tab_ptr, |
| &std::unique_ptr<sessions::SessionTab>::get); |
| if (tab_iter != existing_window_tabs.end()) { |
| existing_window_tabs.erase(tab_iter); |
| break; |
| } |
| } |
| } |
| |
| session->synced_tab_map.erase(new_tab_iter); |
| } |
| |
| // If the old tab is unmapped, update the tab id under which it is |
| // indexed. |
| auto unmapped_tabs_iter = session->unmapped_tabs.find(old_tab_id); |
| if (old_tab_id.is_valid() && |
| unmapped_tabs_iter != session->unmapped_tabs.end()) { |
| std::unique_ptr<sessions::SessionTab> tab = |
| std::move(unmapped_tabs_iter->second); |
| DCHECK_EQ(tab_ptr, tab.get()); |
| session->unmapped_tabs.erase(unmapped_tabs_iter); |
| session->unmapped_tabs[new_tab_id] = std::move(tab); |
| } |
| } |
| |
| if (tab_ptr == nullptr) { |
| // It's possible a placeholder is already in place for the new tab. If so, |
| // reuse it, otherwise create a new one (which will default to unmapped). |
| tab_ptr = GetTab(local_session_tag_, new_tab_id); |
| } |
| |
| // Update the tab id. |
| if (old_tab_id.is_valid()) { |
| DVLOG(1) << "Remapped tab " << old_tab_id << " with node " << tab_node_id |
| << " to tab " << new_tab_id; |
| } else { |
| DVLOG(1) << "Mapped new tab node " << tab_node_id << " to tab " |
| << new_tab_id; |
| } |
| tab_ptr->tab_id = new_tab_id; |
| |
| // Add the tab back into the tab map with the new id. |
| session->synced_tab_map[new_tab_id] = tab_ptr; |
| } |
| |
| void SyncedSessionTracker::Clear() { |
| session_map_.clear(); |
| local_session_tag_.clear(); |
| } |
| |
| void UpdateTrackerWithSpecifics(const sync_pb::SessionSpecifics& specifics, |
| base::Time modification_time, |
| SyncedSessionTracker* tracker) { |
| std::string session_tag = specifics.session_tag(); |
| SyncedSession* session = tracker->GetSession(session_tag); |
| if (specifics.has_header()) { |
| // Read in the header data for this session. Header data is |
| // essentially a collection of windows, each of which has an ordered id list |
| // for their tabs. |
| |
| if (!IsValidSessionHeader(specifics.header())) { |
| DLOG(WARNING) << "Ignoring session node with invalid header " |
| << "and tag " << session_tag << "."; |
| return; |
| } |
| |
| // Load (or create) the SyncedSession object for this client. |
| const sync_pb::SessionHeader& header = specifics.header(); |
| |
| // Reset the tab/window tracking for this session (must do this before |
| // we start calling PutWindowInSession and PutTabInWindow so that all |
| // unused tabs/windows get cleared by the CleanupSession(...) call). |
| tracker->ResetSessionTracking(session_tag); |
| |
| PopulateSyncedSessionFromSpecifics(session_tag, header, modification_time, |
| session, tracker); |
| |
| // Delete any closed windows and unused tabs as necessary. We exclude the |
| // local session here because it should be cleaned up explicitly with |
| // CleanupLocalTabs(). |
| if (session_tag != tracker->GetLocalSessionTag()) { |
| tracker->CleanupSession(session_tag); |
| } |
| } else if (specifics.has_tab()) { |
| const sync_pb::SessionTab& tab_s = specifics.tab(); |
| SessionID tab_id = SessionID::FromSerializedValue(tab_s.tab_id()); |
| if (!tab_id.is_valid()) { |
| DLOG(WARNING) << "Ignoring session tab with invalid tab ID for session " |
| << "tag " << session_tag << " and node ID " |
| << specifics.tab_node_id() << "."; |
| return; |
| } |
| |
| DVLOG(1) << "Populating " << session_tag << "'s tab id " << tab_id |
| << " from node " << specifics.tab_node_id(); |
| |
| // Ensure the tracker is aware of the tab node id. Deleting foreign sessions |
| // requires deleting all relevant tab nodes, and it's easier to track the |
| // tab node ids themselves separately from the tab ids. |
| // |
| // Note that TabIDs are not stable across restarts of a client. Consider |
| // this example with two tabs: |
| // |
| // http://a.com TabID1 --> NodeIDA |
| // http://b.com TabID2 --> NodeIDB |
| // |
| // After restart, tab ids are reallocated. e.g, one possibility: |
| // http://a.com TabID2 --> NodeIDA |
| // http://b.com TabID1 --> NodeIDB |
| // |
| // If that happened on a remote client, here we will see an update to |
| // TabID1 with tab_node_id changing from NodeIDA to NodeIDB, and TabID2 |
| // with tab_node_id changing from NodeIDB to NodeIDA. |
| // |
| // We can also wind up here if we created this tab as an out-of-order |
| // update to the header node for this session before actually associating |
| // the tab itself, so the tab node id wasn't available at the time and |
| // is currently kInvalidTabNodeID. |
| // |
| // In both cases, we can safely throw it into the set of node ids. |
| tracker->OnTabNodeSeen(session_tag, specifics.tab_node_id(), tab_id); |
| sessions::SessionTab* tab = tracker->GetTab(session_tag, tab_id); |
| if (!tab->timestamp.is_null() && tab->timestamp > modification_time) { |
| DVLOG(1) << "Ignoring " << session_tag << "'s session tab " << tab_id |
| << " with earlier modification time: " << tab->timestamp |
| << " vs " << modification_time; |
| return; |
| } |
| |
| // Update SessionTab based on protobuf. |
| SetSessionTabFromSyncData(tab_s, modification_time, tab); |
| |
| // Update the last modified time. |
| if (session->GetModifiedTime() < modification_time) { |
| session->SetModifiedTime(modification_time); |
| } |
| } else { |
| LOG(WARNING) << "Ignoring session node with missing header/tab " |
| << "fields and tag " << session_tag << "."; |
| } |
| } |
| |
| void SerializeTrackerToSpecifics( |
| const SyncedSessionTracker& tracker, |
| const base::RepeatingCallback<void(const std::string& session_name, |
| sync_pb::SessionSpecifics* specifics)>& |
| output_cb) { |
| std::map<std::string, std::set<int>> session_tag_to_node_ids; |
| for (const SyncedSession* session : |
| tracker.LookupAllSessions(SyncedSessionTracker::RAW)) { |
| // Request all tabs. |
| session_tag_to_node_ids[session->GetSessionTag()] = |
| tracker.LookupTabNodeIds(session->GetSessionTag()); |
| // Request the header too. |
| session_tag_to_node_ids[session->GetSessionTag()].insert( |
| TabNodePool::kInvalidTabNodeID); |
| } |
| SerializePartialTrackerToSpecifics(tracker, session_tag_to_node_ids, |
| output_cb); |
| } |
| |
| void SerializePartialTrackerToSpecifics( |
| const SyncedSessionTracker& tracker, |
| const std::map<std::string, std::set<int>>& session_tag_to_node_ids, |
| const base::RepeatingCallback<void(const std::string& session_name, |
| sync_pb::SessionSpecifics* specifics)>& |
| output_cb) { |
| for (const auto& [session_tag, node_ids] : session_tag_to_node_ids) { |
| const SyncedSession* session = tracker.LookupSession(session_tag); |
| if (!session) { |
| // Unknown session. |
| continue; |
| } |
| |
| const std::set<int> known_tab_node_ids = |
| tracker.LookupTabNodeIds(session_tag); |
| |
| for (int tab_node_id : node_ids) { |
| // Header entity. |
| if (tab_node_id == TabNodePool::kInvalidTabNodeID) { |
| sync_pb::SessionSpecifics header_pb; |
| header_pb.set_session_tag(session_tag); |
| session->ToSessionHeaderProto().Swap(header_pb.mutable_header()); |
| output_cb.Run(session->GetSessionName(), &header_pb); |
| continue; |
| } |
| |
| // Check if |tab_node_id| is known by the tracker. |
| if (known_tab_node_ids.count(tab_node_id) == 0) { |
| continue; |
| } |
| |
| // Tab entities. |
| const SessionID tab_id = |
| tracker.LookupTabIdFromTabNodeId(session_tag, tab_node_id); |
| if (!tab_id.is_valid()) { |
| // This can be the case for tabs that were unmapped because we received |
| // a new foreign tab with the same tab ID (the last one wins), so we |
| // don't remember the tab ID for the original |tab_node_id|. Instead of |
| // returning partially populated SessionSpecifics (without tab ID), we |
| // simply drop them, because older clients don't handle well such |
| // invalid specifics. |
| continue; |
| } |
| |
| const sessions::SessionTab* tab = |
| tracker.LookupSessionTab(session_tag, tab_id); |
| if (tab) { |
| // Associated/mapped tab node. |
| sync_pb::SessionSpecifics tab_pb; |
| tab_pb.set_session_tag(session_tag); |
| tab_pb.set_tab_node_id(tab_node_id); |
| *tab_pb.mutable_tab() = SessionTabToSyncData( |
| *tab, tracker.LookupWindowType(session_tag, tab->window_id)); |
| output_cb.Run(session->GetSessionName(), &tab_pb); |
| continue; |
| } |
| |
| // Create entities for unmapped tabs nodes. |
| sync_pb::SessionSpecifics tab_pb; |
| tab_pb.set_tab_node_id(tab_node_id); |
| tab_pb.set_session_tag(session_tag); |
| tab_pb.mutable_tab()->set_tab_id(tab_id.id()); |
| output_cb.Run(session->GetSessionName(), &tab_pb); |
| } |
| } |
| } |
| |
| } // namespace sync_sessions |