Skip to main content

fractal/session_list/
mod.rs

1use std::{cmp::Ordering, ffi::OsString};
2
3use gettextrs::gettext;
4use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
5use indexmap::map::IndexMap;
6use tracing::{error, info};
7
8mod failed_session;
9mod new_session;
10mod session_info;
11mod session_list_settings;
12
13pub(crate) use self::{
14    failed_session::*, new_session::*, session_info::*, session_list_settings::*,
15};
16use crate::{
17    prelude::*,
18    secret::{Secret, StoredSession},
19    session::{Session, SessionState},
20    spawn, spawn_tokio,
21    utils::{DataType, LoadingState},
22};
23
24mod imp {
25    use std::{
26        cell::{Cell, RefCell},
27        marker::PhantomData,
28    };
29
30    use super::*;
31
32    #[derive(Debug, Default, glib::Properties)]
33    #[properties(wrapper_type = super::SessionList)]
34    pub struct SessionList {
35        /// The map of session ID to session.
36        pub(super) list: RefCell<IndexMap<String, SessionInfo>>,
37        /// The loading state of the list.
38        #[property(get, builder(LoadingState::default()))]
39        state: Cell<LoadingState>,
40        /// The error message, if state is set to `LoadingState::Error`.
41        #[property(get, nullable)]
42        error: RefCell<Option<String>>,
43        /// The settings of the sessions.
44        #[property(get)]
45        settings: SessionListSettings,
46        /// Whether this list is empty.
47        #[property(get = Self::is_empty)]
48        is_empty: PhantomData<bool>,
49    }
50
51    #[glib::object_subclass]
52    impl ObjectSubclass for SessionList {
53        const NAME: &'static str = "SessionList";
54        type Type = super::SessionList;
55        type Interfaces = (gio::ListModel,);
56    }
57
58    #[glib::derived_properties]
59    impl ObjectImpl for SessionList {}
60
61    impl ListModelImpl for SessionList {
62        fn item_type(&self) -> glib::Type {
63            SessionInfo::static_type()
64        }
65
66        fn n_items(&self) -> u32 {
67            self.list.borrow().len() as u32
68        }
69
70        fn item(&self, position: u32) -> Option<glib::Object> {
71            self.list
72                .borrow()
73                .get_index(position as usize)
74                .map(|(_, v)| v.clone().upcast())
75        }
76    }
77
78    impl SessionList {
79        /// Whether this list is empty.
80        fn is_empty(&self) -> bool {
81            self.list.borrow().is_empty()
82        }
83
84        /// Set the loading state of this list.
85        fn set_state(&self, state: LoadingState) {
86            if self.state.get() == state {
87                return;
88            }
89
90            self.state.set(state);
91            self.obj().notify_state();
92        }
93
94        /// Set the error message.
95        fn set_error(&self, message: String) {
96            self.error.replace(Some(message));
97
98            self.obj().notify_error();
99            self.set_state(LoadingState::Error);
100        }
101
102        /// Insert the given session into the list.
103        ///
104        /// If a session with the same ID already exists, it is replaced.
105        ///
106        /// Returns the index of the session.
107        pub(super) fn insert(&self, session: impl IsA<SessionInfo>) -> usize {
108            let session = session.upcast();
109
110            if let Some(session) = session.downcast_ref::<Session>() {
111                session.connect_logged_out(clone!(
112                    #[weak(rename_to = imp)]
113                    self,
114                    move |session| imp.remove(session.session_id())
115                ));
116            }
117
118            let was_empty = self.is_empty();
119
120            let (index, replaced) = self
121                .list
122                .borrow_mut()
123                .insert_full(session.session_id(), session);
124
125            let removed = replaced.is_some().into();
126
127            let obj = self.obj();
128            obj.items_changed(index as u32, removed, 1);
129
130            if was_empty {
131                obj.notify_is_empty();
132            }
133
134            index
135        }
136
137        /// Remove the session with the given ID from the list.
138        fn remove(&self, session_id: &str) {
139            let removed = self.list.borrow_mut().shift_remove_full(session_id);
140
141            if let Some((position, ..)) = removed {
142                let obj = self.obj();
143                obj.items_changed(position as u32, 1, 0);
144
145                if self.is_empty() {
146                    obj.notify_is_empty();
147                }
148            }
149        }
150
151        /// Restore the logged-in sessions.
152        pub(super) async fn restore_sessions(&self) {
153            if self.state.get() >= LoadingState::Loading {
154                return;
155            }
156
157            self.set_state(LoadingState::Loading);
158
159            let mut sessions = match Secret::restore_sessions().await {
160                Ok(sessions) => sessions,
161                Err(error) => {
162                    let message = format!(
163                        "{}\n\n{}",
164                        gettext("Could not restore previous sessions"),
165                        error.to_user_facing(),
166                    );
167
168                    self.set_error(message);
169                    return;
170                }
171            };
172
173            self.settings.load();
174            let session_ids = self.settings.session_ids();
175
176            // Keep the order from the settings.
177            sessions.sort_by(|a, b| {
178                let pos_a = session_ids.get_index_of(&a.id);
179                let pos_b = session_ids.get_index_of(&b.id);
180
181                match (pos_a, pos_b) {
182                    (Some(pos_a), Some(pos_b)) => pos_a.cmp(&pos_b),
183                    // Keep unknown sessions at the end.
184                    (Some(_), None) => Ordering::Greater,
185                    (None, Some(_)) => Ordering::Less,
186                    _ => Ordering::Equal,
187                }
188            });
189
190            // Get the directories present in the data path to only restore sessions with
191            // data on the system. This is necessary for users sharing their secrets between
192            // devices.
193            let mut directories = match self.data_directories(sessions.len()).await {
194                Ok(directories) => directories,
195                Err(error) => {
196                    error!("Could not access data directory: {error}");
197                    let message = format!(
198                        "{}\n\n{}",
199                        gettext("Could not restore previous sessions"),
200                        gettext("An unexpected error happened while accessing the data directory"),
201                    );
202
203                    self.set_error(message);
204                    return;
205                }
206            };
207
208            for stored_session in sessions {
209                if let Some(pos) = directories
210                    .iter()
211                    .position(|dir_name| dir_name == stored_session.id.as_str())
212                {
213                    directories.swap_remove(pos);
214                    info!(
215                        "Restoring previous session {} for user {}",
216                        stored_session.id, stored_session.user_id,
217                    );
218                    self.insert(NewSession::new(&stored_session));
219
220                    spawn!(
221                        glib::Priority::DEFAULT_IDLE,
222                        clone!(
223                            #[weak(rename_to = obj)]
224                            self,
225                            async move {
226                                obj.restore_stored_session(&stored_session).await;
227                            }
228                        )
229                    );
230                } else {
231                    info!(
232                        "Ignoring session {} for user {}: no data directory",
233                        stored_session.id, stored_session.user_id,
234                    );
235                }
236            }
237
238            self.set_state(LoadingState::Ready);
239        }
240
241        /// The list of directories in the data directory.
242        async fn data_directories(&self, capacity: usize) -> std::io::Result<Vec<OsString>> {
243            let data_path = DataType::Persistent.dir_path();
244
245            if !data_path.try_exists()? {
246                return Ok(Vec::new());
247            }
248
249            spawn_tokio!(async move {
250                let mut read_dir = tokio::fs::read_dir(data_path).await?;
251                let mut directories = Vec::with_capacity(capacity);
252
253                loop {
254                    let Some(entry) = read_dir.next_entry().await? else {
255                        // We are at the end of the list.
256                        break;
257                    };
258
259                    if !entry.file_type().await?.is_dir() {
260                        // We are only interested in directories.
261                        continue;
262                    }
263
264                    directories.push(entry.file_name());
265                }
266
267                std::io::Result::Ok(directories)
268            })
269            .await
270            .expect("task was not aborted")
271        }
272
273        /// Restore a stored session.
274        async fn restore_stored_session(&self, session_info: &StoredSession) {
275            let settings = self.settings.get_or_create(&session_info.id);
276            match Session::new(session_info.clone(), settings).await {
277                Ok(session) => {
278                    session.prepare().await;
279                    self.insert(session);
280                }
281                Err(error) => {
282                    error!("Could not restore previous session: {error}");
283                    self.insert(FailedSession::new(session_info, error));
284                }
285            }
286        }
287    }
288}
289
290glib::wrapper! {
291    /// List of all logged in sessions.
292    pub struct SessionList(ObjectSubclass<imp::SessionList>)
293        @implements gio::ListModel;
294}
295
296impl SessionList {
297    /// Create a new empty `SessionList`.
298    pub fn new() -> Self {
299        glib::Object::new()
300    }
301
302    /// Whether at least one session is ready.
303    pub(crate) fn has_session_ready(&self) -> bool {
304        self.imp()
305            .list
306            .borrow()
307            .values()
308            .filter_map(|s| s.downcast_ref::<Session>())
309            .any(|s| s.state() == SessionState::Ready)
310    }
311
312    /// The session with the given ID, if any.
313    pub(crate) fn get(&self, session_id: &str) -> Option<SessionInfo> {
314        self.imp().list.borrow().get(session_id).cloned()
315    }
316
317    /// The index of the session with the given ID, if any.
318    pub(crate) fn index(&self, session_id: &str) -> Option<usize> {
319        self.imp().list.borrow().get_index_of(session_id)
320    }
321
322    /// The first session in the list, if any.
323    pub(crate) fn first(&self) -> Option<SessionInfo> {
324        self.imp().list.borrow().first().map(|(_, v)| v.clone())
325    }
326
327    /// Insert the given session into the list.
328    ///
329    /// If a session with the same ID already exists, it is replaced.
330    ///
331    /// Returns the index of the session.
332    pub(crate) fn insert(&self, session: impl IsA<SessionInfo>) -> usize {
333        self.imp().insert(session)
334    }
335
336    /// Restore the logged-in sessions.
337    pub(crate) async fn restore_sessions(&self) {
338        self.imp().restore_sessions().await;
339    }
340}
341
342impl Default for SessionList {
343    fn default() -> Self {
344        Self::new()
345    }
346}