|
|
|
|
@ -30,19 +30,20 @@ mod imp {
|
|
|
|
|
#[derive(Debug, Default, glib::Properties)] |
|
|
|
|
#[properties(wrapper_type = super::SessionList)] |
|
|
|
|
pub struct SessionList { |
|
|
|
|
/// The map of session ID to session.
|
|
|
|
|
pub(super) list: RefCell<IndexMap<String, SessionInfo>>, |
|
|
|
|
/// The loading state of the list.
|
|
|
|
|
#[property(get, builder(LoadingState::default()))] |
|
|
|
|
pub state: Cell<LoadingState>, |
|
|
|
|
state: Cell<LoadingState>, |
|
|
|
|
/// The error message, if state is set to `LoadingState::Error`.
|
|
|
|
|
#[property(get, nullable)] |
|
|
|
|
pub error: RefCell<Option<String>>, |
|
|
|
|
/// A map of session ID to session.
|
|
|
|
|
pub list: RefCell<IndexMap<String, SessionInfo>>, |
|
|
|
|
error: RefCell<Option<String>>, |
|
|
|
|
/// The settings of the sessions.
|
|
|
|
|
pub settings: SessionListSettings, |
|
|
|
|
#[property(get)] |
|
|
|
|
settings: SessionListSettings, |
|
|
|
|
/// Whether this list is empty.
|
|
|
|
|
#[property(get = Self::is_empty)] |
|
|
|
|
pub is_empty: PhantomData<bool>, |
|
|
|
|
is_empty: PhantomData<bool>, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[glib::object_subclass] |
|
|
|
|
@ -68,8 +69,7 @@ mod imp {
|
|
|
|
|
self.list |
|
|
|
|
.borrow() |
|
|
|
|
.get_index(position as usize) |
|
|
|
|
.map(|(_, v)| v.upcast_ref::<glib::Object>()) |
|
|
|
|
.cloned() |
|
|
|
|
.map(|(_, v)| v.clone().upcast()) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -78,6 +78,211 @@ mod imp {
|
|
|
|
|
fn is_empty(&self) -> bool { |
|
|
|
|
self.list.borrow().is_empty() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Set the loading state of this list.
|
|
|
|
|
fn set_state(&self, state: LoadingState) { |
|
|
|
|
if self.state.get() == state { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.state.set(state); |
|
|
|
|
self.obj().notify_state(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Set the error message.
|
|
|
|
|
fn set_error(&self, message: String) { |
|
|
|
|
self.error.replace(Some(message)); |
|
|
|
|
self.obj().notify_error(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Insert the given session into the list.
|
|
|
|
|
///
|
|
|
|
|
/// If a session with the same ID already exists, it is replaced.
|
|
|
|
|
///
|
|
|
|
|
/// Returns the index of the session.
|
|
|
|
|
pub(super) fn insert(&self, session: impl IsA<SessionInfo>) -> usize { |
|
|
|
|
let session = session.upcast(); |
|
|
|
|
|
|
|
|
|
if let Some(session) = session.downcast_ref::<Session>() { |
|
|
|
|
session.connect_logged_out(clone!( |
|
|
|
|
#[weak(rename_to = imp)] |
|
|
|
|
self, |
|
|
|
|
move |session| imp.remove(session.session_id()) |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let was_empty = self.is_empty(); |
|
|
|
|
|
|
|
|
|
let (index, replaced) = self |
|
|
|
|
.list |
|
|
|
|
.borrow_mut() |
|
|
|
|
.insert_full(session.session_id(), session); |
|
|
|
|
|
|
|
|
|
let removed = replaced.is_some().into(); |
|
|
|
|
|
|
|
|
|
let obj = self.obj(); |
|
|
|
|
obj.items_changed(index as u32, removed, 1); |
|
|
|
|
|
|
|
|
|
if was_empty { |
|
|
|
|
obj.notify_is_empty(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
index |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Remove the session with the given ID from the list.
|
|
|
|
|
fn remove(&self, session_id: &str) { |
|
|
|
|
let removed = self.list.borrow_mut().shift_remove_full(session_id); |
|
|
|
|
|
|
|
|
|
if let Some((position, ..)) = removed { |
|
|
|
|
let obj = self.obj(); |
|
|
|
|
obj.items_changed(position as u32, 1, 0); |
|
|
|
|
|
|
|
|
|
if self.is_empty() { |
|
|
|
|
obj.notify_is_empty(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Restore the logged-in sessions.
|
|
|
|
|
pub(super) async fn restore_sessions(&self) { |
|
|
|
|
if self.state.get() >= LoadingState::Loading { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.set_state(LoadingState::Loading); |
|
|
|
|
|
|
|
|
|
let handle = spawn_tokio!(secret::restore_sessions()); |
|
|
|
|
let mut sessions = match handle.await.expect("task was not aborted") { |
|
|
|
|
Ok(sessions) => sessions, |
|
|
|
|
Err(error) => { |
|
|
|
|
let message = format!( |
|
|
|
|
"{}\n\n{}", |
|
|
|
|
gettext("Could not restore previous sessions"), |
|
|
|
|
error.to_user_facing(), |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
self.set_error(message); |
|
|
|
|
self.set_state(LoadingState::Error); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
self.settings.load(); |
|
|
|
|
let session_ids = self.settings.session_ids(); |
|
|
|
|
|
|
|
|
|
// Keep the order from the settings.
|
|
|
|
|
sessions.sort_by(|a, b| { |
|
|
|
|
let pos_a = session_ids.get_index_of(&a.id); |
|
|
|
|
let pos_b = session_ids.get_index_of(&b.id); |
|
|
|
|
|
|
|
|
|
match (pos_a, pos_b) { |
|
|
|
|
(Some(pos_a), Some(pos_b)) => pos_a.cmp(&pos_b), |
|
|
|
|
// Keep unknown sessions at the end.
|
|
|
|
|
(Some(_), None) => Ordering::Greater, |
|
|
|
|
(None, Some(_)) => Ordering::Less, |
|
|
|
|
_ => Ordering::Equal, |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Get the directories present in the data path to only restore sessions with
|
|
|
|
|
// data on the system. This is necessary for users sharing their secrets between
|
|
|
|
|
// devices.
|
|
|
|
|
let mut directories = match self.data_directories(sessions.len()).await { |
|
|
|
|
Ok(directories) => directories, |
|
|
|
|
Err(error) => { |
|
|
|
|
error!("Could not access data directory: {error}"); |
|
|
|
|
let message = format!( |
|
|
|
|
"{}\n\n{}", |
|
|
|
|
gettext("Could not restore previous sessions"), |
|
|
|
|
gettext("An unexpected error happened while accessing the data directory"), |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
self.set_error(message); |
|
|
|
|
self.set_state(LoadingState::Error); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
for stored_session in sessions { |
|
|
|
|
if let Some(pos) = directories |
|
|
|
|
.iter() |
|
|
|
|
.position(|dir_name| dir_name == stored_session.id.as_str()) |
|
|
|
|
{ |
|
|
|
|
directories.swap_remove(pos); |
|
|
|
|
info!( |
|
|
|
|
"Restoring previous session {} for user {}", |
|
|
|
|
stored_session.id, stored_session.user_id, |
|
|
|
|
); |
|
|
|
|
self.insert(NewSession::new(&stored_session)); |
|
|
|
|
|
|
|
|
|
spawn!( |
|
|
|
|
glib::Priority::DEFAULT_IDLE, |
|
|
|
|
clone!( |
|
|
|
|
#[weak(rename_to = obj)] |
|
|
|
|
self, |
|
|
|
|
async move { |
|
|
|
|
obj.restore_stored_session(&stored_session).await; |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
} else { |
|
|
|
|
info!( |
|
|
|
|
"Ignoring session {} for user {}: no data directory", |
|
|
|
|
stored_session.id, stored_session.user_id, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.set_state(LoadingState::Ready); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// The list of directories in the data directory.
|
|
|
|
|
async fn data_directories(&self, capacity: usize) -> std::io::Result<Vec<OsString>> { |
|
|
|
|
let data_path = data_dir_path(DataType::Persistent); |
|
|
|
|
|
|
|
|
|
if !data_path.try_exists()? { |
|
|
|
|
return Ok(Vec::new()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
spawn_tokio!(async move { |
|
|
|
|
let mut read_dir = tokio::fs::read_dir(data_path).await?; |
|
|
|
|
let mut directories = Vec::with_capacity(capacity); |
|
|
|
|
|
|
|
|
|
loop { |
|
|
|
|
let Some(entry) = read_dir.next_entry().await? else { |
|
|
|
|
// We are at the end of the list.
|
|
|
|
|
break; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if !entry.file_type().await?.is_dir() { |
|
|
|
|
// We are only interested in directories.
|
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
directories.push(entry.file_name()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::io::Result::Ok(directories) |
|
|
|
|
}) |
|
|
|
|
.await |
|
|
|
|
.expect("task was not aborted") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Restore a stored session.
|
|
|
|
|
async fn restore_stored_session(&self, session_info: &StoredSession) { |
|
|
|
|
let settings = self.settings.get_or_create(&session_info.id); |
|
|
|
|
match Session::restore(session_info.clone(), settings).await { |
|
|
|
|
Ok(session) => { |
|
|
|
|
session.prepare().await; |
|
|
|
|
self.insert(session); |
|
|
|
|
} |
|
|
|
|
Err(error) => { |
|
|
|
|
error!("Could not restore previous session: {error}"); |
|
|
|
|
self.insert(FailedSession::new(session_info, error)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -93,38 +298,8 @@ impl SessionList {
|
|
|
|
|
glib::Object::new() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Set the loading state of this list.
|
|
|
|
|
fn set_state(&self, state: LoadingState) { |
|
|
|
|
if self.state() == state { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.imp().state.set(state); |
|
|
|
|
self.notify_state(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Set the error message.
|
|
|
|
|
fn set_error(&self, message: String) { |
|
|
|
|
self.imp().error.replace(Some(message)); |
|
|
|
|
self.notify_error(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// The settings of the sessions.
|
|
|
|
|
pub fn settings(&self) -> &SessionListSettings { |
|
|
|
|
&self.imp().settings |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Whether any of the sessions are new.
|
|
|
|
|
pub fn has_new_sessions(&self) -> bool { |
|
|
|
|
self.imp() |
|
|
|
|
.list |
|
|
|
|
.borrow() |
|
|
|
|
.values() |
|
|
|
|
.any(ObjectExt::is::<NewSession>) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Whether at least one session is ready.
|
|
|
|
|
pub fn has_session_ready(&self) -> bool { |
|
|
|
|
pub(crate) fn has_session_ready(&self) -> bool { |
|
|
|
|
self.imp() |
|
|
|
|
.list |
|
|
|
|
.borrow() |
|
|
|
|
@ -134,207 +309,32 @@ impl SessionList {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// The session with the given ID, if any.
|
|
|
|
|
pub fn get(&self, session_id: &str) -> Option<SessionInfo> { |
|
|
|
|
pub(crate) fn get(&self, session_id: &str) -> Option<SessionInfo> { |
|
|
|
|
self.imp().list.borrow().get(session_id).cloned() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// The index of the session with the given ID, if any.
|
|
|
|
|
pub fn index(&self, session_id: &str) -> Option<usize> { |
|
|
|
|
pub(crate) fn index(&self, session_id: &str) -> Option<usize> { |
|
|
|
|
self.imp().list.borrow().get_index_of(session_id) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// The first session in the list, if any.
|
|
|
|
|
pub fn first(&self) -> Option<SessionInfo> { |
|
|
|
|
pub(crate) fn first(&self) -> Option<SessionInfo> { |
|
|
|
|
self.imp().list.borrow().first().map(|(_, v)| v.clone()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Insert the given session to the list.
|
|
|
|
|
/// Insert the given session into the list.
|
|
|
|
|
///
|
|
|
|
|
/// If a session with the same ID already exists, it is replaced.
|
|
|
|
|
///
|
|
|
|
|
/// Returns the index of the session.
|
|
|
|
|
pub fn insert(&self, session: impl IsA<SessionInfo>) -> usize { |
|
|
|
|
let session = session.upcast(); |
|
|
|
|
|
|
|
|
|
if let Some(session) = session.downcast_ref::<Session>() { |
|
|
|
|
session.connect_logged_out(clone!( |
|
|
|
|
#[weak(rename_to = obj)] |
|
|
|
|
self, |
|
|
|
|
move |session| obj.remove(session.session_id()) |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let was_empty = self.is_empty(); |
|
|
|
|
|
|
|
|
|
let (index, replaced) = self |
|
|
|
|
.imp() |
|
|
|
|
.list |
|
|
|
|
.borrow_mut() |
|
|
|
|
.insert_full(session.session_id(), session); |
|
|
|
|
|
|
|
|
|
let removed = replaced.is_some().into(); |
|
|
|
|
|
|
|
|
|
self.items_changed(index as u32, removed, 1); |
|
|
|
|
|
|
|
|
|
if was_empty { |
|
|
|
|
self.notify_is_empty(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
index |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Remove the session with the given ID from the list.
|
|
|
|
|
pub fn remove(&self, session_id: &str) { |
|
|
|
|
let removed = self.imp().list.borrow_mut().shift_remove_full(session_id); |
|
|
|
|
|
|
|
|
|
if let Some((position, ..)) = removed { |
|
|
|
|
self.items_changed(position as u32, 1, 0); |
|
|
|
|
|
|
|
|
|
if self.is_empty() { |
|
|
|
|
self.notify_is_empty(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
pub(crate) fn insert(&self, session: impl IsA<SessionInfo>) -> usize { |
|
|
|
|
self.imp().insert(session) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Restore the logged-in sessions.
|
|
|
|
|
pub async fn restore_sessions(&self) { |
|
|
|
|
if self.state() >= LoadingState::Loading { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.set_state(LoadingState::Loading); |
|
|
|
|
|
|
|
|
|
let handle = spawn_tokio!(secret::restore_sessions()); |
|
|
|
|
let mut sessions = match handle.await.unwrap() { |
|
|
|
|
Ok(sessions) => sessions, |
|
|
|
|
Err(error) => { |
|
|
|
|
let message = format!( |
|
|
|
|
"{}\n\n{}", |
|
|
|
|
gettext("Could not restore previous sessions"), |
|
|
|
|
error.to_user_facing(), |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
self.set_error(message); |
|
|
|
|
self.set_state(LoadingState::Error); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let settings = self.settings(); |
|
|
|
|
settings.load(); |
|
|
|
|
let session_ids = settings.session_ids(); |
|
|
|
|
|
|
|
|
|
// Keep the order from the settings.
|
|
|
|
|
sessions.sort_by(|a, b| { |
|
|
|
|
let pos_a = session_ids.get_index_of(&a.id); |
|
|
|
|
let pos_b = session_ids.get_index_of(&b.id); |
|
|
|
|
|
|
|
|
|
match (pos_a, pos_b) { |
|
|
|
|
(Some(pos_a), Some(pos_b)) => pos_a.cmp(&pos_b), |
|
|
|
|
// Keep unknown sessions at the end.
|
|
|
|
|
(Some(_), None) => Ordering::Greater, |
|
|
|
|
(None, Some(_)) => Ordering::Less, |
|
|
|
|
_ => Ordering::Equal, |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Get the directories present in the data path to only restore sessions with
|
|
|
|
|
// data on the system. This is necessary for users sharing their secrets between
|
|
|
|
|
// devices.
|
|
|
|
|
let mut directories = match self.data_directories(sessions.len()).await { |
|
|
|
|
Ok(directories) => directories, |
|
|
|
|
Err(error) => { |
|
|
|
|
error!("Could not access data directory: {error}"); |
|
|
|
|
let message = format!( |
|
|
|
|
"{}\n\n{}", |
|
|
|
|
gettext("Could not restore previous sessions"), |
|
|
|
|
gettext("An unexpected error happened while accessing the data directory"), |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
self.set_error(message); |
|
|
|
|
self.set_state(LoadingState::Error); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
for stored_session in sessions { |
|
|
|
|
if let Some(pos) = directories |
|
|
|
|
.iter() |
|
|
|
|
.position(|dir_name| dir_name == stored_session.id.as_str()) |
|
|
|
|
{ |
|
|
|
|
directories.swap_remove(pos); |
|
|
|
|
info!( |
|
|
|
|
"Restoring previous session {} for user {}", |
|
|
|
|
stored_session.id, stored_session.user_id, |
|
|
|
|
); |
|
|
|
|
self.insert(NewSession::new(&stored_session)); |
|
|
|
|
|
|
|
|
|
spawn!( |
|
|
|
|
glib::Priority::DEFAULT_IDLE, |
|
|
|
|
clone!( |
|
|
|
|
#[weak(rename_to = obj)] |
|
|
|
|
self, |
|
|
|
|
async move { |
|
|
|
|
obj.restore_stored_session(&stored_session).await; |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
} else { |
|
|
|
|
info!( |
|
|
|
|
"Ignoring session {} for user {}: no data directory", |
|
|
|
|
stored_session.id, stored_session.user_id, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.set_state(LoadingState::Ready); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// The list of directories in the data directory.
|
|
|
|
|
async fn data_directories(&self, capacity: usize) -> std::io::Result<Vec<OsString>> { |
|
|
|
|
let data_path = data_dir_path(DataType::Persistent); |
|
|
|
|
|
|
|
|
|
if !data_path.try_exists()? { |
|
|
|
|
return Ok(Vec::new()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
spawn_tokio!(async move { |
|
|
|
|
let mut read_dir = tokio::fs::read_dir(data_path).await?; |
|
|
|
|
let mut directories = Vec::with_capacity(capacity); |
|
|
|
|
|
|
|
|
|
loop { |
|
|
|
|
let Some(entry) = read_dir.next_entry().await? else { |
|
|
|
|
// We are at the end of the list.
|
|
|
|
|
break; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if !entry.file_type().await?.is_dir() { |
|
|
|
|
// We are only interested in directories.
|
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
directories.push(entry.file_name()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::io::Result::Ok(directories) |
|
|
|
|
}) |
|
|
|
|
.await |
|
|
|
|
.expect("task was not aborted") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Restore a stored session.
|
|
|
|
|
async fn restore_stored_session(&self, session_info: &StoredSession) { |
|
|
|
|
let settings = self.settings().get_or_create(&session_info.id); |
|
|
|
|
match Session::restore(session_info.clone(), settings).await { |
|
|
|
|
Ok(session) => { |
|
|
|
|
session.prepare().await; |
|
|
|
|
self.insert(session); |
|
|
|
|
} |
|
|
|
|
Err(error) => { |
|
|
|
|
error!("Could not restore previous session: {error}"); |
|
|
|
|
self.insert(FailedSession::new(session_info, error)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
pub(crate) async fn restore_sessions(&self) { |
|
|
|
|
self.imp().restore_sessions().await; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|