build_binary_package installed its ephemeral chroot context into the process-global manager and read it back with context::current(), ignoring its ctx parameter: two concurrent builds would re-point each other's global and each drop would clean up whichever chroot was current at the time. The guard now keeps the Arc of the context it created (parented directly on the base context, not on a config-name lookup), exposes it via context(), and Drop cleans up exactly that context and restores the exact handle that was current at creation, so overlapping builds no longer cross-destroy each other.
186 lines
5.8 KiB
Rust
186 lines
5.8 KiB
Rust
use directories::ProjectDirs;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::io;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::sync::RwLock;
|
|
|
|
use super::api::{Context, ContextConfig};
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct Config {
|
|
pub context: String,
|
|
pub contexts: HashMap<String, ContextConfig>,
|
|
}
|
|
|
|
impl Default for Config {
|
|
fn default() -> Self {
|
|
let mut contexts = HashMap::new();
|
|
contexts.insert("local".to_string(), ContextConfig::Local);
|
|
Self {
|
|
context: "local".to_string(),
|
|
contexts,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper managing contexts
|
|
pub struct ContextManager {
|
|
context: RwLock<Arc<Context>>,
|
|
config_path: PathBuf,
|
|
config: RwLock<Config>,
|
|
}
|
|
|
|
pub static MANAGER: std::sync::LazyLock<ContextManager> =
|
|
std::sync::LazyLock::new(|| ContextManager::new().expect("Cannot setup context manager"));
|
|
|
|
impl ContextManager {
|
|
fn new() -> io::Result<Self> {
|
|
let proj_dirs = ProjectDirs::from("com", "pkh", "pkh").ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::NotFound,
|
|
"Could not determine config directory",
|
|
)
|
|
})?;
|
|
let config_dir = proj_dirs.config_dir();
|
|
fs::create_dir_all(config_dir)?;
|
|
let config_path = config_dir.join("contexts.json");
|
|
|
|
let config = if config_path.exists() {
|
|
// Load existing configuration file
|
|
let content = fs::read_to_string(&config_path)?;
|
|
serde_json::from_str(&content)
|
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
|
|
} else {
|
|
// Create a new configuration file
|
|
Config::default()
|
|
};
|
|
|
|
Ok(Self {
|
|
context: RwLock::new(Arc::new(Self::make_context(
|
|
config.context.as_str(),
|
|
&config,
|
|
))),
|
|
config_path,
|
|
config: RwLock::new(config),
|
|
})
|
|
}
|
|
|
|
/// Obtain current ContextManager configuration
|
|
pub fn get_config(&self) -> std::sync::RwLockReadGuard<'_, Config> {
|
|
self.config.read().unwrap()
|
|
}
|
|
|
|
/// Make a ContextManager using a specific configuration path
|
|
pub fn with_path(path: PathBuf) -> Self {
|
|
let config = Config::default();
|
|
Self {
|
|
context: RwLock::new(Arc::new(Self::make_context("local", &config))),
|
|
config_path: path,
|
|
config: RwLock::new(config),
|
|
}
|
|
}
|
|
|
|
/// Save current context configuration to disk
|
|
pub fn save(&self) -> io::Result<()> {
|
|
let config = self.config.read().unwrap();
|
|
let content = serde_json::to_string_pretty(&*config)
|
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
|
fs::write(&self.config_path, content)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn make_context(name: &str, config: &Config) -> Context {
|
|
let context_config = config
|
|
.contexts
|
|
.get(name)
|
|
.cloned()
|
|
.expect("Context not found in config");
|
|
Context::new(context_config)
|
|
}
|
|
|
|
/// List contexts from configuration
|
|
pub fn list_contexts(&self) -> Vec<String> {
|
|
self.config
|
|
.read()
|
|
.unwrap()
|
|
.contexts
|
|
.keys()
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
|
|
/// Add a context to configuration
|
|
pub fn add_context(&self, name: &str, config: ContextConfig) -> io::Result<()> {
|
|
self.config
|
|
.write()
|
|
.unwrap()
|
|
.contexts
|
|
.insert(name.to_string(), config);
|
|
self.save()
|
|
}
|
|
|
|
/// Remove context from configuration
|
|
pub fn remove_context(&self, name: &str) -> io::Result<()> {
|
|
let mut config = self.config.write().unwrap();
|
|
if name == "local" {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"Cannot remove local context",
|
|
));
|
|
}
|
|
if config.contexts.remove(name).is_some() {
|
|
// If we are removing the current context, fallback to local
|
|
if name == config.context {
|
|
config.context = "local".to_string();
|
|
self.set_current_ephemeral(Self::make_context("local", &config));
|
|
}
|
|
|
|
drop(config); // Drop write lock before saving
|
|
self.save()?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Set current context from name (modifying configuration)
|
|
pub fn set_current(&self, name: &str) -> io::Result<()> {
|
|
let mut config = self.config.write().unwrap();
|
|
if config.contexts.contains_key(name) {
|
|
config.context = name.to_string();
|
|
self.set_current_ephemeral(Self::make_context(name, &config));
|
|
drop(config); // Drop write lock before saving
|
|
self.save()?;
|
|
Ok(())
|
|
} else {
|
|
Err(io::Error::new(
|
|
io::ErrorKind::NotFound,
|
|
format!("Context '{}' not found", name),
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Set current context, without modifying configuration
|
|
///
|
|
/// Accepts either an owned [`Context`] or an already-shared
|
|
/// `Arc<Context>`: callers that keep their own handle to the context
|
|
/// they install (e.g. [`crate::deb::ephemeral::EphemeralContextGuard`])
|
|
/// pass the Arc so they can restore exactly this context afterwards
|
|
/// instead of relying on whatever happens to be current at that time.
|
|
pub fn set_current_ephemeral(&self, context: impl Into<Arc<Context>>) {
|
|
*self.context.write().unwrap() = context.into();
|
|
}
|
|
|
|
/// Obtain current context handle
|
|
pub fn current(&self) -> Arc<Context> {
|
|
self.context.read().unwrap().clone()
|
|
}
|
|
|
|
/// Obtain current context name
|
|
/// Will not work for ephemeral context (obtained from config)
|
|
pub fn current_name(&self) -> String {
|
|
self.config.read().unwrap().context.clone()
|
|
}
|
|
}
|