OLD | NEW |
---|---|
1 // Copyright 2012 The Chromium Authors. All rights reserved. | 1 // Copyright 2012 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "components/sessions/core/persistent_tab_restore_service.h" | 5 #include "components/sessions/core/persistent_tab_restore_service.h" |
6 | 6 |
7 #include <stddef.h> | 7 #include <stddef.h> |
8 #include <stdint.h> | 8 #include <stdint.h> |
9 #include <string.h> | 9 #include <string.h> |
10 #include <utility> | 10 #include <utility> |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
44 | 44 |
45 // Payload used for the start of a window close. This is the old struct that is | 45 // Payload used for the start of a window close. This is the old struct that is |
46 // used for backwards compat when it comes to reading the session files. This | 46 // used for backwards compat when it comes to reading the session files. This |
47 // struct must be POD, because we memset the contents. | 47 // struct must be POD, because we memset the contents. |
48 struct WindowPayload { | 48 struct WindowPayload { |
49 SessionID::id_type window_id; | 49 SessionID::id_type window_id; |
50 int32_t selected_tab_index; | 50 int32_t selected_tab_index; |
51 int32_t num_tabs; | 51 int32_t num_tabs; |
52 }; | 52 }; |
53 | 53 |
54 // Payload used for the start of a window close. This struct must be POD, | 54 // Payload used for the start of a window close. This struct must be POD, |
55 // because we memset the contents. | 55 // because we memset the contents. This is an older version of the struct that |
56 // is used for backwards compat when it comes to reading the session files. | |
56 struct WindowPayload2 : WindowPayload { | 57 struct WindowPayload2 : WindowPayload { |
sky
2017/06/28 22:38:01
How about adding Obsolete as a suffix to this and
chrisha
2017/06/30 15:42:12
Done.
| |
57 int64_t timestamp; | 58 int64_t timestamp; |
58 }; | 59 }; |
59 | 60 |
60 // Payload used for the start of a tab close. | 61 // Payload used for the start of a tab close. |
61 struct SelectedNavigationInTabPayload2 : SelectedNavigationInTabPayload { | 62 struct SelectedNavigationInTabPayload2 : SelectedNavigationInTabPayload { |
62 int64_t timestamp; | 63 int64_t timestamp; |
63 }; | 64 }; |
64 | 65 |
65 // Used to indicate what has loaded. | 66 // Used to indicate what has loaded. |
66 enum LoadState { | 67 enum LoadState { |
(...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
128 // Erase it if it's our target. | 129 // Erase it if it's our target. |
129 if (tab.id == id) { | 130 if (tab.id == id) { |
130 window.tabs.erase(it); | 131 window.tabs.erase(it); |
131 return; | 132 return; |
132 } | 133 } |
133 } | 134 } |
134 } | 135 } |
135 } | 136 } |
136 } | 137 } |
137 | 138 |
139 // Superset of WindowPayload/WindowPayload2 and the other fields that can appear | |
140 // in the Pickle version of a Window command. | |
141 struct WindowCommandFields { | |
142 // Fields in WindowPayload/WindowPayload2/Pickle: | |
143 SessionID::id_type window_id = 0; | |
144 int32_t selected_tab_index = 0; | |
145 int32_t num_tabs = 0; | |
146 | |
147 // Fields in WindowPayload2/Pickle: | |
148 int64_t timestamp = 0; | |
149 | |
150 // Fields in Pickle: | |
151 // Completely zeroed position/dimensions indicates that defaults should be | |
152 // used. | |
153 int32_t window_x = 0; | |
154 int32_t window_y = 0; | |
155 int32_t window_width = 0; | |
156 int32_t window_height = 0; | |
157 // Use the default show state if none is specified. | |
158 int32_t window_show_state = static_cast<int32_t>(ui::SHOW_STATE_DEFAULT); | |
159 std::string workspace; | |
160 }; | |
161 | |
162 std::unique_ptr<sessions::TabRestoreService::Window> | |
163 CreateWindowEntryFromCommand(const SessionCommand* command, | |
164 SessionID::id_type* window_id, | |
165 int32_t* num_tabs) { | |
166 bool parsed = false; | |
167 | |
168 WindowCommandFields fields; | |
169 | |
170 // First try to parse the command as a pickle (the most modern version). | |
171 std::unique_ptr<base::Pickle> pickle(command->PayloadAsPickle()); | |
172 if (pickle.get()) { | |
173 base::PickleIterator it(*pickle); | |
174 WindowCommandFields f; | |
sky
2017/06/28 22:38:00
Please use a more readable name then 'f'.
chrisha
2017/06/30 15:42:12
Done.
| |
175 | |
176 // The first version of the pickle contains all of the following fields, so | |
177 // they should all successfully parse if the command is in fact a pickle. | |
178 if (it.ReadInt(&f.window_id) && it.ReadInt(&f.selected_tab_index) && | |
179 it.ReadInt(&f.num_tabs) && it.ReadInt64(&f.timestamp) && | |
180 it.ReadInt(&f.window_x) && it.ReadInt(&f.window_y) && | |
181 it.ReadInt(&f.window_width) && it.ReadInt(&f.window_height) && | |
182 it.ReadInt(&f.window_show_state) && it.ReadString(&f.workspace)) { | |
183 // New fields added to the pickle in later versions would be parsed here. | |
184 | |
185 // Copy the parsed data. | |
186 fields = f; | |
187 parsed = true; | |
188 } | |
189 } | |
190 | |
191 // Next try to parse the command as a WindowPayload2. | |
192 if (!parsed) { | |
193 WindowPayload2 payload2; | |
194 if (command->GetPayload(&payload2, sizeof(payload2))) { | |
195 fields.window_id = payload2.window_id; | |
196 fields.selected_tab_index = payload2.selected_tab_index; | |
197 fields.num_tabs = payload2.num_tabs; | |
198 fields.timestamp = payload2.timestamp; | |
199 parsed = true; | |
200 } | |
201 } | |
202 | |
203 // Finally, try the oldest WindowPayload type. | |
204 if (!parsed) { | |
205 WindowPayload payload; | |
206 if (command->GetPayload(&payload, sizeof(payload))) { | |
207 fields.window_id = payload.window_id; | |
208 fields.selected_tab_index = payload.selected_tab_index; | |
209 fields.num_tabs = payload.num_tabs; | |
210 parsed = true; | |
211 } | |
212 } | |
213 | |
214 if (!parsed) | |
215 return nullptr; | |
216 | |
217 // Create the Window entry. | |
218 std::unique_ptr<sessions::TabRestoreService::Window> window = | |
219 base::MakeUnique<sessions::TabRestoreService::Window>(); | |
220 window->selected_tab_index = fields.selected_tab_index; | |
221 window->timestamp = base::Time::FromInternalValue(fields.timestamp); | |
222 window->selected_tab_index = fields.selected_tab_index; | |
223 window->timestamp = base::Time::FromInternalValue(fields.timestamp); | |
224 *window_id = fields.window_id; | |
225 *num_tabs = fields.num_tabs; | |
226 | |
227 // Set the bounds, show state and workspace if valid ones have been provided. | |
228 if (!(fields.window_x == 0 && fields.window_y == 0 && | |
229 fields.window_width == 0 && fields.window_height == 0)) { | |
230 window->bounds.SetRect(fields.window_x, fields.window_y, | |
231 fields.window_width, fields.window_height); | |
232 window->show_state = | |
233 static_cast<ui::WindowShowState>(fields.window_show_state); | |
sky
2017/06/28 22:38:00
You need to validate the enum.
chrisha
2017/06/30 15:42:13
Done.
| |
234 window->workspace = std::move(fields.workspace); | |
235 } | |
236 | |
237 return window; | |
238 } | |
239 | |
138 } // namespace | 240 } // namespace |
139 | 241 |
140 // PersistentTabRestoreService::Delegate --------------------------------------- | 242 // PersistentTabRestoreService::Delegate --------------------------------------- |
141 | 243 |
142 // This restore service will create and own a BaseSessionService and implement | 244 // This restore service will create and own a BaseSessionService and implement |
143 // the required BaseSessionServiceDelegate. | 245 // the required BaseSessionServiceDelegate. |
144 class PersistentTabRestoreService::Delegate | 246 class PersistentTabRestoreService::Delegate |
145 : public BaseSessionServiceDelegate, | 247 : public BaseSessionServiceDelegate, |
146 public TabRestoreServiceHelper::Observer { | 248 public TabRestoreServiceHelper::Observer { |
147 public: | 249 public: |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
180 | 282 |
181 // Schedules the commands for a window close. | 283 // Schedules the commands for a window close. |
182 void ScheduleCommandsForWindow(const Window& window); | 284 void ScheduleCommandsForWindow(const Window& window); |
183 | 285 |
184 // Schedules the commands for a tab close. |selected_index| gives the index of | 286 // Schedules the commands for a tab close. |selected_index| gives the index of |
185 // the selected navigation. | 287 // the selected navigation. |
186 void ScheduleCommandsForTab(const Tab& tab, int selected_index); | 288 void ScheduleCommandsForTab(const Tab& tab, int selected_index); |
187 | 289 |
188 // Creates a window close command. | 290 // Creates a window close command. |
189 static std::unique_ptr<SessionCommand> CreateWindowCommand( | 291 static std::unique_ptr<SessionCommand> CreateWindowCommand( |
190 SessionID::id_type id, | 292 SessionID::id_type window_id, |
191 int selected_tab_index, | 293 int selected_tab_index, |
192 int num_tabs, | 294 int num_tabs, |
295 const gfx::Rect& bounds, | |
296 ui::WindowShowState show_state, | |
297 const std::string& workspace, | |
193 base::Time timestamp); | 298 base::Time timestamp); |
194 | 299 |
195 // Creates a tab close command. | 300 // Creates a tab close command. |
196 static std::unique_ptr<SessionCommand> CreateSelectedNavigationInTabCommand( | 301 static std::unique_ptr<SessionCommand> CreateSelectedNavigationInTabCommand( |
197 SessionID::id_type tab_id, | 302 SessionID::id_type tab_id, |
198 int32_t index, | 303 int32_t index, |
199 base::Time timestamp); | 304 base::Time timestamp); |
200 | 305 |
201 // Creates a restore command. | 306 // Creates a restore command. |
202 static std::unique_ptr<SessionCommand> CreateRestoredEntryCommand( | 307 static std::unique_ptr<SessionCommand> CreateRestoredEntryCommand( |
(...skipping 225 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
428 valid_tab_count++; | 533 valid_tab_count++; |
429 } else if (static_cast<int>(i) < selected_tab) { | 534 } else if (static_cast<int>(i) < selected_tab) { |
430 real_selected_tab--; | 535 real_selected_tab--; |
431 } | 536 } |
432 } | 537 } |
433 if (valid_tab_count == 0) | 538 if (valid_tab_count == 0) |
434 return; // No tabs to persist. | 539 return; // No tabs to persist. |
435 | 540 |
436 base_session_service_->ScheduleCommand(CreateWindowCommand( | 541 base_session_service_->ScheduleCommand(CreateWindowCommand( |
437 window.id, std::min(real_selected_tab, valid_tab_count - 1), | 542 window.id, std::min(real_selected_tab, valid_tab_count - 1), |
438 valid_tab_count, window.timestamp)); | 543 valid_tab_count, window.bounds, window.show_state, window.workspace, |
544 window.timestamp)); | |
439 | 545 |
440 if (!window.app_name.empty()) { | 546 if (!window.app_name.empty()) { |
441 base_session_service_->ScheduleCommand(CreateSetWindowAppNameCommand( | 547 base_session_service_->ScheduleCommand(CreateSetWindowAppNameCommand( |
442 kCommandSetWindowAppName, window.id, window.app_name)); | 548 kCommandSetWindowAppName, window.id, window.app_name)); |
443 } | 549 } |
444 | 550 |
445 for (size_t i = 0; i < window.tabs.size(); ++i) { | 551 for (size_t i = 0; i < window.tabs.size(); ++i) { |
446 int selected_index = GetSelectedNavigationIndexToPersist(*window.tabs[i]); | 552 int selected_index = GetSelectedNavigationIndexToPersist(*window.tabs[i]); |
447 if (selected_index != -1) | 553 if (selected_index != -1) |
448 ScheduleCommandsForTab(*window.tabs[i], selected_index); | 554 ScheduleCommandsForTab(*window.tabs[i], selected_index); |
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
497 CreateUpdateTabNavigationCommand(kCommandUpdateTabNavigation, | 603 CreateUpdateTabNavigationCommand(kCommandUpdateTabNavigation, |
498 tab.id, | 604 tab.id, |
499 navigations[i])); | 605 navigations[i])); |
500 } | 606 } |
501 } | 607 } |
502 } | 608 } |
503 | 609 |
504 // static | 610 // static |
505 std::unique_ptr<SessionCommand> | 611 std::unique_ptr<SessionCommand> |
506 PersistentTabRestoreService::Delegate::CreateWindowCommand( | 612 PersistentTabRestoreService::Delegate::CreateWindowCommand( |
507 SessionID::id_type id, | 613 SessionID::id_type window_id, |
508 int selected_tab_index, | 614 int selected_tab_index, |
509 int num_tabs, | 615 int num_tabs, |
616 const gfx::Rect& bounds, | |
617 ui::WindowShowState show_state, | |
618 const std::string& workspace, | |
510 base::Time timestamp) { | 619 base::Time timestamp) { |
511 WindowPayload2 payload; | 620 // Use a pickle to handle marshaling as this command contains variable-length |
512 // |timestamp| is aligned on a 16 byte boundary, leaving 4 bytes of | 621 // content. |
513 // uninitialized memory in the struct. | 622 base::Pickle pickle; |
514 memset(&payload, 0, sizeof(payload)); | 623 pickle.WriteInt(window_id); |
sky
2017/06/28 22:38:00
You need to make sure the writing, reading and rep
chrisha
2017/06/30 15:42:12
Done.
| |
515 payload.window_id = id; | 624 pickle.WriteInt(selected_tab_index); |
516 payload.selected_tab_index = selected_tab_index; | 625 pickle.WriteInt(num_tabs); |
517 payload.num_tabs = num_tabs; | 626 pickle.WriteInt64(timestamp.ToInternalValue()); |
518 payload.timestamp = timestamp.ToInternalValue(); | 627 pickle.WriteInt(bounds.x()); |
628 pickle.WriteInt(bounds.y()); | |
629 pickle.WriteInt(bounds.width()); | |
630 pickle.WriteInt(bounds.height()); | |
631 pickle.WriteInt(static_cast<int32_t>(show_state)); | |
sky
2017/06/28 22:38:00
I would prefer you mirror WindowShowState locally.
chrisha
2017/06/30 15:42:12
Done.
| |
632 | |
633 // Enforce a maximum length on workspace names. A common size is 32 bytes for | |
634 // GUIDs. | |
635 if (workspace.size() <= 128) { | |
sky
2017/06/28 22:38:00
no {}
chrisha
2017/06/30 15:42:13
Done.
| |
636 pickle.WriteString(workspace); | |
637 } else { | |
638 pickle.WriteString(std::string()); | |
639 } | |
519 | 640 |
520 std::unique_ptr<SessionCommand> command( | 641 std::unique_ptr<SessionCommand> command( |
521 new SessionCommand(kCommandWindow, sizeof(payload))); | 642 new SessionCommand(kCommandWindow, pickle)); |
522 memcpy(command->contents(), &payload, sizeof(payload)); | |
523 return command; | 643 return command; |
524 } | 644 } |
525 | 645 |
526 // static | 646 // static |
527 std::unique_ptr<SessionCommand> | 647 std::unique_ptr<SessionCommand> |
528 PersistentTabRestoreService::Delegate::CreateSelectedNavigationInTabCommand( | 648 PersistentTabRestoreService::Delegate::CreateSelectedNavigationInTabCommand( |
529 SessionID::id_type tab_id, | 649 SessionID::id_type tab_id, |
530 int32_t index, | 650 int32_t index, |
531 base::Time timestamp) { | 651 base::Time timestamp) { |
532 SelectedNavigationInTabPayload2 payload; | 652 SelectedNavigationInTabPayload2 payload; |
(...skipping 85 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
618 current_window = nullptr; | 738 current_window = nullptr; |
619 | 739 |
620 RestoredEntryPayload payload; | 740 RestoredEntryPayload payload; |
621 if (!command.GetPayload(&payload, sizeof(payload))) | 741 if (!command.GetPayload(&payload, sizeof(payload))) |
622 return; | 742 return; |
623 RemoveEntryByID(payload, &entries); | 743 RemoveEntryByID(payload, &entries); |
624 break; | 744 break; |
625 } | 745 } |
626 | 746 |
627 case kCommandWindow: { | 747 case kCommandWindow: { |
628 WindowPayload2 payload; | 748 // Should never receive a window command while waiting for all the |
629 if (pending_window_tabs > 0) { | 749 // tabs in a window. |
630 // Should never receive a window command while waiting for all the | 750 if (pending_window_tabs > 0) |
631 // tabs in a window. | |
632 return; | 751 return; |
633 } | |
634 | 752 |
635 // Try the new payload first | 753 // Try to parse the command, and silently skip if it fails. |
636 if (!command.GetPayload(&payload, sizeof(payload))) { | 754 int32_t num_tabs = 0; |
637 // then the old payload | 755 SessionID::id_type window_id = 0; |
638 WindowPayload old_payload; | 756 std::unique_ptr<Window> window = |
639 if (!command.GetPayload(&old_payload, sizeof(old_payload))) | 757 CreateWindowEntryFromCommand(&command, &window_id, &num_tabs); |
640 return; | 758 if (!window) |
759 return; | |
641 | 760 |
642 // Copy the old payload data to the new payload. | 761 // Should always have at least 1 tab. Likely indicates corruption. |
643 payload.window_id = old_payload.window_id; | 762 pending_window_tabs = num_tabs; |
644 payload.selected_tab_index = old_payload.selected_tab_index; | 763 if (pending_window_tabs <= 0) |
645 payload.num_tabs = old_payload.num_tabs; | 764 return; |
646 // Since we don't have a time use time 0 which is used to mark as an | |
647 // unknown timestamp. | |
648 payload.timestamp = 0; | |
649 } | |
650 | 765 |
651 pending_window_tabs = payload.num_tabs; | 766 RemoveEntryByID(window_id, &entries); |
652 if (pending_window_tabs <= 0) { | 767 current_window = window.get(); |
653 // Should always have at least 1 tab. Likely indicates corruption. | 768 entries.push_back(std::move(window)); |
654 return; | |
655 } | |
656 | |
657 RemoveEntryByID(payload.window_id, &entries); | |
658 | |
659 entries.push_back(base::MakeUnique<Window>()); | |
660 current_window = static_cast<Window*>(entries.back().get()); | |
661 current_window->selected_tab_index = payload.selected_tab_index; | |
662 current_window->timestamp = | |
663 base::Time::FromInternalValue(payload.timestamp); | |
664 break; | 769 break; |
665 } | 770 } |
666 | 771 |
667 case kCommandSelectedNavigationInTab: { | 772 case kCommandSelectedNavigationInTab: { |
668 SelectedNavigationInTabPayload2 payload; | 773 SelectedNavigationInTabPayload2 payload; |
669 if (!command.GetPayload(&payload, sizeof(payload))) { | 774 if (!command.GetPayload(&payload, sizeof(payload))) { |
670 SelectedNavigationInTabPayload old_payload; | 775 SelectedNavigationInTabPayload old_payload; |
671 if (!command.GetPayload(&old_payload, sizeof(old_payload))) | 776 if (!command.GetPayload(&old_payload, sizeof(old_payload))) |
672 return; | 777 return; |
673 payload.id = old_payload.id; | 778 payload.id = old_payload.id; |
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
766 if (!RestoreSetTabUserAgentOverrideCommand(command, | 871 if (!RestoreSetTabUserAgentOverrideCommand(command, |
767 &tab_id, | 872 &tab_id, |
768 &user_agent_override)) { | 873 &user_agent_override)) { |
769 return; | 874 return; |
770 } | 875 } |
771 current_tab->user_agent_override.swap(user_agent_override); | 876 current_tab->user_agent_override.swap(user_agent_override); |
772 break; | 877 break; |
773 } | 878 } |
774 | 879 |
775 default: | 880 default: |
776 // Unknown type, usually indicates corruption of file. Ignore it. | |
sky
2017/06/28 22:38:00
Why are you removing this comment?
chrisha
2017/06/30 15:42:12
Oops, stray line removal. Restored.
| |
777 return; | 881 return; |
778 } | 882 } |
779 } | 883 } |
780 | 884 |
781 // If there was corruption some of the entries won't be valid. | 885 // If there was corruption some of the entries won't be valid. |
782 ValidateAndDeleteEmptyEntries(&entries); | 886 ValidateAndDeleteEmptyEntries(&entries); |
783 | |
784 loaded_entries->swap(entries); | 887 loaded_entries->swap(entries); |
785 } | 888 } |
786 | 889 |
787 // static | 890 // static |
788 void PersistentTabRestoreService::Delegate::ValidateAndDeleteEmptyEntries( | 891 void PersistentTabRestoreService::Delegate::ValidateAndDeleteEmptyEntries( |
789 std::vector<std::unique_ptr<Entry>>* entries) { | 892 std::vector<std::unique_ptr<Entry>>* entries) { |
790 std::vector<std::unique_ptr<Entry>> valid_entries; | 893 std::vector<std::unique_ptr<Entry>> valid_entries; |
791 | 894 |
792 // Iterate from the back so that we keep the most recently closed entries. | 895 // Iterate from the back so that we keep the most recently closed entries. |
793 for (auto i = entries->rbegin(); i != entries->rend(); ++i) { | 896 for (auto i = entries->rbegin(); i != entries->rend(); ++i) { |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
826 tab.timestamp = base::Time(); | 929 tab.timestamp = base::Time(); |
827 } | 930 } |
828 } | 931 } |
829 if (window->tabs.empty()) | 932 if (window->tabs.empty()) |
830 return false; | 933 return false; |
831 | 934 |
832 window->selected_tab_index = | 935 window->selected_tab_index = |
833 std::min(session_window->selected_tab_index, | 936 std::min(session_window->selected_tab_index, |
834 static_cast<int>(window->tabs.size() - 1)); | 937 static_cast<int>(window->tabs.size() - 1)); |
835 window->timestamp = base::Time(); | 938 window->timestamp = base::Time(); |
939 window->bounds = session_window->bounds; | |
940 window->show_state = session_window->show_state; | |
941 window->workspace = session_window->workspace; | |
836 return true; | 942 return true; |
837 } | 943 } |
838 | 944 |
839 void PersistentTabRestoreService::Delegate::LoadStateChanged() { | 945 void PersistentTabRestoreService::Delegate::LoadStateChanged() { |
840 if ((load_state_ & (LOADED_LAST_TABS | LOADED_LAST_SESSION)) != | 946 if ((load_state_ & (LOADED_LAST_TABS | LOADED_LAST_SESSION)) != |
841 (LOADED_LAST_TABS | LOADED_LAST_SESSION)) { | 947 (LOADED_LAST_TABS | LOADED_LAST_SESSION)) { |
842 // Still waiting on previous session or previous tabs. | 948 // Still waiting on previous session or previous tabs. |
843 return; | 949 return; |
844 } | 950 } |
845 | 951 |
(...skipping 118 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
964 | 1070 |
965 TabRestoreService::Entries* PersistentTabRestoreService::mutable_entries() { | 1071 TabRestoreService::Entries* PersistentTabRestoreService::mutable_entries() { |
966 return &helper_.entries_; | 1072 return &helper_.entries_; |
967 } | 1073 } |
968 | 1074 |
969 void PersistentTabRestoreService::PruneEntries() { | 1075 void PersistentTabRestoreService::PruneEntries() { |
970 helper_.PruneEntries(); | 1076 helper_.PruneEntries(); |
971 } | 1077 } |
972 | 1078 |
973 } // namespace sessions | 1079 } // namespace sessions |
OLD | NEW |