use crate::error::{Error, Result};
use crate::profiles::{DEFAULT_PROFILE, PROFILES_DIR, validate_profile_name};
use crate::utils::sync::RwLockExt;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
pub type InvalidateCallback = Arc<dyn Fn() + Send + Sync>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileManifest {
pub active: String,
pub profiles: Vec<String>,
}
impl Default for ProfileManifest {
fn default() -> Self {
Self {
active: DEFAULT_PROFILE.to_string(),
profiles: vec![DEFAULT_PROFILE.to_string()],
}
}
}
impl ProfileManifest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn has_profile(&self, name: &str) -> bool {
self.profiles.iter().any(|p| p == name)
}
pub fn add_profile(&mut self, name: String) {
if !self.has_profile(&name) {
self.profiles.push(name);
}
}
pub fn remove_profile(&mut self, name: &str) -> bool {
if let Some(pos) = self.profiles.iter().position(|p| p == name) {
self.profiles.remove(pos);
true
} else {
false
}
}
pub fn rename_profile(&mut self, from: &str, to: String) -> bool {
if let Some(pos) = self.profiles.iter().position(|p| p == from) {
self.profiles[pos].clone_from(&to);
if self.active == from {
self.active = to;
}
true
} else {
false
}
}
pub fn set_active(&mut self, name: &str) -> bool {
if self.has_profile(name) {
self.active = name.to_string();
true
} else {
false
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProfileEvent {
Switched {
from: String,
to: String,
},
Created {
name: String,
},
Deleted {
name: String,
},
Renamed {
from: String,
to: String,
},
Duplicated {
source: String,
target: String,
},
}
use crate::storage::StorageBackend;
pub type ProfileEventCallback = Arc<dyn Fn(ProfileEvent) + Send + Sync>;
pub struct ProfileManager<S: StorageBackend = crate::storage::JsonStorage> {
manifest_path: PathBuf,
profiles_dir: PathBuf,
target_name: String,
storage: S,
manifest: RwLock<Option<ProfileManifest>>,
on_event: RwLock<Option<ProfileEventCallback>>,
on_invalidate: RwLock<Option<InvalidateCallback>>,
}
impl<S: StorageBackend> ProfileManager<S> {
pub fn new(base_dir: &Path, target_name: impl Into<String>, storage: S) -> Self {
let filename = format!(".profiles.{}", storage.extension());
Self {
manifest_path: base_dir.join(filename),
profiles_dir: base_dir.join(PROFILES_DIR),
target_name: target_name.into(),
storage,
manifest: RwLock::new(None),
on_event: RwLock::new(None),
on_invalidate: RwLock::new(None),
}
}
pub fn initialize(
config_dir: &Path,
target_name: &str,
storage: S,
enabled: bool,
migrator: &crate::profiles::ProfileMigrator,
) -> Result<(PathBuf, Option<Self>)> {
if enabled {
crate::profiles::migrate(config_dir, target_name, false, &storage, migrator)?;
let pm = Self::new(config_dir, target_name, storage);
let active = pm.active_path()?;
info!(
"Profiles initialized for '{target_name}' (active: {})",
active.display()
);
Ok((active, Some(pm)))
} else {
Ok((config_dir.to_path_buf(), None))
}
}
pub fn set_on_event<F>(&self, callback: F)
where
F: Fn(ProfileEvent) + Send + Sync + 'static,
{
if let Ok(mut guard) = self.on_event.write_recovered() {
*guard = Some(Arc::new(callback));
} else {
debug!(
"Failed to register profile event callback for '{}' due to lock recovery error",
self.target_name
);
}
}
pub fn set_on_invalidate<F>(&self, callback: F)
where
F: Fn() + Send + Sync + 'static,
{
if let Ok(mut guard) = self.on_invalidate.write_recovered() {
*guard = Some(Arc::new(callback));
} else {
debug!(
"Failed to register profile invalidation callback for '{}' due to lock recovery error",
self.target_name
);
}
}
fn emit_event(&self, event: ProfileEvent) {
if let Ok(guard) = self.on_event.read_recovered() {
if let Some(callback) = guard.as_ref() {
callback(event);
}
} else {
debug!(
"Failed to emit profile event for '{}' due to lock recovery error",
self.target_name
);
}
}
fn invalidate_caches(&self) {
if let Ok(guard) = self.on_invalidate.read_recovered() {
if let Some(callback) = guard.as_ref() {
callback();
}
} else {
debug!(
"Failed to run profile invalidation callback for '{}' due to lock recovery error",
self.target_name
);
}
}
pub fn invalidate_manifest(&self) {
if let Ok(mut guard) = self.manifest.write_recovered() {
*guard = None;
} else {
debug!(
"Failed to invalidate profile manifest cache for '{}' due to lock recovery error",
self.target_name
);
}
}
pub fn profile_path(&self, name: &str) -> PathBuf {
self.profiles_dir.join(name)
}
pub fn active_path(&self) -> Result<PathBuf> {
let active = self.active()?;
Ok(self.profile_path(&active))
}
fn ensure_manifest(&self) -> Result<()> {
{
let guard = self.manifest.read_recovered()?;
if guard.is_some() {
return Ok(());
}
}
let mut guard = self.manifest.write_recovered()?;
if guard.is_some() {
return Ok(());
}
let load_result = if self.manifest_path.exists() {
self.storage.read(&self.manifest_path).map(Some)
} else {
Ok(None)
};
match load_result {
Ok(Some(manifest)) => {
*guard = Some(manifest);
}
Ok(None) => {
*guard = Some(ProfileManifest::default());
}
Err(e) => return Err(e),
}
Ok(())
}
fn save_manifest(&self) -> Result<()> {
let manifest_clone = {
let guard = self.manifest.read_recovered()?;
let manifest = guard.as_ref().ok_or(Error::NotInitialized)?;
manifest.clone()
};
self.storage.write(&self.manifest_path, &manifest_clone)?;
debug!(
"Saved profile manifest for '{}': active={}",
self.target_name, manifest_clone.active
);
Ok(())
}
pub fn active(&self) -> Result<String> {
self.ensure_manifest()?;
let guard = self.manifest.read_recovered()?;
Ok(guard.as_ref().ok_or(Error::NotInitialized)?.active.clone())
}
pub fn list(&self) -> Result<Vec<String>> {
self.ensure_manifest()?;
let guard = self.manifest.read_recovered()?;
Ok(guard
.as_ref()
.ok_or(Error::NotInitialized)?
.profiles
.clone())
}
pub fn exists(&self, name: &str) -> Result<bool> {
self.ensure_manifest()?;
let guard = self.manifest.read_recovered()?;
Ok(guard
.as_ref()
.ok_or(Error::NotInitialized)?
.has_profile(name))
}
pub fn create(&self, name: &str) -> Result<()> {
validate_profile_name(name)?;
self.ensure_manifest()?;
{
let guard = self.manifest.read_recovered()?;
if guard
.as_ref()
.ok_or(Error::NotInitialized)?
.has_profile(name)
{
return Err(Error::ProfileAlreadyExists(name.to_string()));
}
}
let profile_dir = self.profile_path(name);
crate::utils::security::ensure_secure_dir(&profile_dir)?;
{
let mut guard = self.manifest.write_recovered()?;
guard
.as_mut()
.ok_or(Error::NotInitialized)?
.add_profile(name.to_string());
}
self.save_manifest()?;
info!("Created profile '{}' for '{}'", name, self.target_name);
self.emit_event(ProfileEvent::Created {
name: name.to_string(),
});
Ok(())
}
pub fn switch(&self, name: &str) -> Result<()> {
self.ensure_manifest()?;
let from = {
let guard = self.manifest.read_recovered()?;
let manifest = guard.as_ref().ok_or(Error::NotInitialized)?;
if !manifest.has_profile(name) {
return Err(Error::ProfileNotFound(name.to_string()));
}
manifest.active.clone()
};
if from == name {
debug!("Profile '{name}' is already active");
return Ok(());
}
{
let mut guard = self.manifest.write_recovered()?;
guard
.as_mut()
.ok_or(Error::NotInitialized)?
.set_active(name);
}
self.save_manifest()?;
info!(
"Switched profile for '{}': {} -> {}",
self.target_name, from, name
);
self.invalidate_caches();
self.emit_event(ProfileEvent::Switched {
from,
to: name.to_string(),
});
Ok(())
}
pub fn delete(&self, name: &str) -> Result<()> {
self.ensure_manifest()?;
{
let guard = self.manifest.read_recovered()?;
let manifest = guard.as_ref().ok_or(Error::NotInitialized)?;
if !manifest.has_profile(name) {
return Err(Error::ProfileNotFound(name.to_string()));
}
if manifest.active == name {
return Err(Error::CannotDeleteActiveProfile(name.to_string()));
}
if manifest.profiles.len() <= 1 {
return Err(Error::CannotDeleteLastProfile);
}
}
let profile_dir = self.profile_path(name);
if profile_dir.exists() {
std::fs::remove_dir_all(&profile_dir).map_err(|e| Error::FileDelete {
path: profile_dir.clone(),
source: e,
})?;
}
{
let mut guard = self.manifest.write_recovered()?;
guard
.as_mut()
.ok_or(Error::NotInitialized)?
.remove_profile(name);
}
self.save_manifest()?;
info!("Deleted profile '{}' from '{}'", name, self.target_name);
self.emit_event(ProfileEvent::Deleted {
name: name.to_string(),
});
Ok(())
}
pub fn rename(&self, from: &str, to: &str) -> Result<()> {
validate_profile_name(to)?;
self.ensure_manifest()?;
{
let guard = self.manifest.read_recovered()?;
let manifest = guard.as_ref().ok_or(Error::NotInitialized)?;
if !manifest.has_profile(from) {
return Err(Error::ProfileNotFound(from.to_string()));
}
if manifest.has_profile(to) {
return Err(Error::ProfileAlreadyExists(to.to_string()));
}
}
let from_dir = self.profile_path(from);
let to_dir = self.profile_path(to);
if from_dir.exists() {
std::fs::rename(&from_dir, &to_dir).map_err(|e| Error::FileWrite {
path: std::path::PathBuf::from(format!(
"{} -> {}",
from_dir.display(),
to_dir.display()
)),
source: e,
})?;
} else {
std::fs::create_dir_all(&to_dir).map_err(|e| Error::DirectoryCreate {
path: to_dir.clone(),
source: e,
})?;
}
{
let mut guard = self.manifest.write_recovered()?;
guard
.as_mut()
.ok_or(Error::NotInitialized)?
.rename_profile(from, to.to_string());
}
self.save_manifest()?;
info!(
"Renamed profile '{}' -> '{}' in '{}'",
from, to, self.target_name
);
self.emit_event(ProfileEvent::Renamed {
from: from.to_string(),
to: to.to_string(),
});
Ok(())
}
pub fn duplicate(&self, source: &str, target: &str) -> Result<()> {
validate_profile_name(target)?;
self.ensure_manifest()?;
{
let guard = self.manifest.read_recovered()?;
let manifest = guard.as_ref().ok_or(Error::NotInitialized)?;
if !manifest.has_profile(source) {
return Err(Error::ProfileNotFound(source.to_string()));
}
if manifest.has_profile(target) {
return Err(Error::ProfileAlreadyExists(target.to_string()));
}
}
let source_dir = self.profile_path(source);
let target_dir = self.profile_path(target);
if source_dir.exists() {
copy_dir_recursive(&source_dir, &target_dir)?;
} else {
std::fs::create_dir_all(&target_dir).map_err(|e| Error::DirectoryCreate {
path: target_dir.clone(),
source: e,
})?;
}
{
let mut guard = self.manifest.write_recovered()?;
guard
.as_mut()
.ok_or(Error::NotInitialized)?
.add_profile(target.to_string());
}
self.save_manifest()?;
info!(
"Duplicated profile '{}' -> '{}' in '{}'",
source, target, self.target_name
);
self.emit_event(ProfileEvent::Duplicated {
source: source.to_string(),
target: target.to_string(),
});
Ok(())
}
pub fn rollback_to_flat(&self) -> Result<()> {
use crate::profiles::rollback_migration;
let root_dir = self
.profiles_dir
.parent()
.ok_or_else(|| Error::Config("Invalid profiles directory structure".to_string()))?;
let single_file_mode = root_dir.with_extension(self.storage.extension()).is_file();
rollback_migration(root_dir, &self.target_name, single_file_mode, &self.storage)?;
self.invalidate_caches();
Ok(())
}
pub fn initialize_with_migration<F>(&self, detect_existing: F) -> Result<bool>
where
F: FnOnce() -> bool,
{
if self.manifest_path.exists() {
self.ensure_manifest()?;
return Ok(false);
}
if !detect_existing() {
self.ensure_manifest()?;
let default_dir = self.profile_path(DEFAULT_PROFILE);
if !default_dir.exists() {
std::fs::create_dir_all(&default_dir).map_err(|e| Error::DirectoryCreate {
path: default_dir.clone(),
source: e,
})?;
}
self.save_manifest()?;
return Ok(false);
}
let mut guard = self.manifest.write_recovered()?;
*guard = Some(ProfileManifest::default());
drop(guard);
if !self.profiles_dir.exists() {
std::fs::create_dir_all(&self.profiles_dir).map_err(|e| Error::DirectoryCreate {
path: self.profiles_dir.clone(),
source: e,
})?;
}
info!(
"Initialized profiles for '{}', migration needed",
self.target_name
);
Ok(true) }
pub fn complete_migration(&self) -> Result<()> {
self.save_manifest()?;
info!("Profile migration complete for '{}'", self.target_name);
Ok(())
}
pub fn manifest(&self) -> Result<ProfileManifest> {
self.ensure_manifest()?;
let guard = self.manifest.read_recovered()?;
Ok(guard.as_ref().ok_or(Error::NotInitialized)?.clone())
}
pub fn profiles_dir(&self) -> &Path {
&self.profiles_dir
}
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
if !dst.exists() {
std::fs::create_dir_all(dst).map_err(|e| Error::DirectoryCreate {
path: dst.to_path_buf(),
source: e,
})?;
}
for entry in std::fs::read_dir(src).map_err(|e| Error::FileRead {
path: src.to_path_buf(),
source: e,
})? {
let entry = entry.map_err(|e| Error::FileRead {
path: src.to_path_buf(),
source: e,
})?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?;
} else {
std::fs::copy(&src_path, &dst_path).map_err(|e| Error::FileWrite {
path: dst_path.clone(),
source: e,
})?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn create_test_manager() -> (
tempfile::TempDir,
ProfileManager<crate::storage::JsonStorage>,
) {
let dir = tempdir().unwrap();
let storage = crate::storage::JsonStorage::compact();
let manager = ProfileManager::new(dir.path(), "test", storage);
(dir, manager)
}
#[test]
fn test_create_profile() {
let (_dir, manager) = create_test_manager();
manager.create("work").unwrap();
let profiles = manager.list().unwrap();
assert!(profiles.contains(&"default".to_string()));
assert!(profiles.contains(&"work".to_string()));
}
#[test]
fn test_switch_profile() {
let (_dir, manager) = create_test_manager();
manager.create("work").unwrap();
manager.switch("work").unwrap();
assert_eq!(manager.active().unwrap(), "work");
}
#[test]
fn test_switch_nonexistent_profile() {
let (_dir, manager) = create_test_manager();
let result = manager.switch("nonexistent");
assert!(matches!(result, Err(Error::ProfileNotFound(_))));
}
#[test]
fn test_delete_profile() {
let (_dir, manager) = create_test_manager();
manager.create("work").unwrap();
manager.delete("work").unwrap();
let profiles = manager.list().unwrap();
assert!(!profiles.contains(&"work".to_string()));
}
#[test]
fn test_cannot_delete_active_profile() {
let (_dir, manager) = create_test_manager();
let result = manager.delete("default");
assert!(matches!(result, Err(Error::CannotDeleteActiveProfile(_))));
}
#[test]
fn test_cannot_delete_last_profile() {
let (_dir, manager) = create_test_manager();
manager.create("work").unwrap();
manager.switch("work").unwrap();
manager.delete("default").unwrap();
let result = manager.delete("work");
assert!(matches!(result, Err(Error::CannotDeleteActiveProfile(_))));
}
#[test]
fn test_rename_profile() {
let (_dir, manager) = create_test_manager();
manager.create("old").unwrap();
manager.rename("old", "new").unwrap();
let profiles = manager.list().unwrap();
assert!(!profiles.contains(&"old".to_string()));
assert!(profiles.contains(&"new".to_string()));
}
#[test]
fn test_duplicate_profile() {
let (dir, manager) = create_test_manager();
manager.create("original").unwrap();
let original_dir = dir.path().join("profiles").join("original");
std::fs::write(original_dir.join("test.json"), r#"{"key": "value"}"#).unwrap();
manager.duplicate("original", "copy").unwrap();
let copy_dir = dir.path().join("profiles").join("copy");
assert!(copy_dir.join("test.json").exists());
}
#[test]
fn test_profile_already_exists() {
let (_dir, manager) = create_test_manager();
manager.create("work").unwrap();
let result = manager.create("work");
assert!(matches!(result, Err(Error::ProfileAlreadyExists(_))));
}
#[test]
fn test_event_callback() {
use std::sync::atomic::{AtomicUsize, Ordering};
let (_dir, manager) = create_test_manager();
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
manager.set_on_event(move |_event| {
counter_clone.fetch_add(1, Ordering::SeqCst);
});
manager.create("work").unwrap();
manager.switch("work").unwrap();
manager.rename("work", "job").unwrap();
assert_eq!(counter.load(Ordering::SeqCst), 3);
}
#[test]
fn test_callback_paths_recover_from_poisoned_locks() {
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicUsize, Ordering};
let (_dir, manager) = create_test_manager();
let _ = catch_unwind(AssertUnwindSafe(|| {
let _guard = manager.on_event.write().unwrap();
panic!("poison on_event lock");
}));
let _ = catch_unwind(AssertUnwindSafe(|| {
let _guard = manager.on_invalidate.write().unwrap();
panic!("poison on_invalidate lock");
}));
let event_count = Arc::new(AtomicUsize::new(0));
let event_count_clone = Arc::clone(&event_count);
manager.set_on_event(move |_event| {
event_count_clone.fetch_add(1, Ordering::SeqCst);
});
let invalidate_count = Arc::new(AtomicUsize::new(0));
let invalidate_count_clone = Arc::clone(&invalidate_count);
manager.set_on_invalidate(move || {
invalidate_count_clone.fetch_add(1, Ordering::SeqCst);
});
manager.create("work").unwrap();
manager.switch("work").unwrap();
assert_eq!(event_count.load(Ordering::SeqCst), 2);
assert_eq!(invalidate_count.load(Ordering::SeqCst), 1);
}
#[test]
fn test_manifest_persistence() {
let dir = tempdir().unwrap();
{
let storage = crate::storage::JsonStorage::compact();
let manager = ProfileManager::new(dir.path(), "test", storage);
manager.create("persistent").unwrap();
}
{
let storage = crate::storage::JsonStorage::compact();
let manager = ProfileManager::new(dir.path(), "test", storage);
let profiles = manager.list().unwrap();
assert!(profiles.contains(&"persistent".to_string()));
}
}
}