use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::trace::trace_lazy;
#[derive(Debug, Clone, Default)]
pub struct ShellSettings {
pub errexit: bool,
pub verbose: bool,
pub xtrace: bool,
pub pipefail: bool,
pub nounset: bool,
pub noglob: bool,
pub allexport: bool,
}
impl ShellSettings {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
*self = Self::default();
}
pub fn set(&mut self, option: &str, value: bool) {
match option {
"e" | "errexit" => self.errexit = value,
"v" | "verbose" => self.verbose = value,
"x" | "xtrace" => self.xtrace = value,
"u" | "nounset" => self.nounset = value,
"f" | "noglob" => self.noglob = value,
"a" | "allexport" => self.allexport = value,
"o pipefail" | "pipefail" => self.pipefail = value,
_ => {
trace_lazy("ShellSettings", || {
format!("Unknown shell option: {}", option)
});
}
}
}
pub fn enable(&mut self, option: &str) {
self.set(option, true);
}
pub fn disable(&mut self, option: &str) {
self.set(option, false);
}
}
pub struct GlobalState {
shell_settings: RwLock<ShellSettings>,
active_runners: RwLock<HashSet<u64>>,
next_runner_id: std::sync::atomic::AtomicU64,
signal_handlers_installed: AtomicBool,
virtual_commands_enabled: AtomicBool,
initial_cwd: RwLock<Option<std::path::PathBuf>>,
}
impl Default for GlobalState {
fn default() -> Self {
Self::new()
}
}
impl GlobalState {
pub fn new() -> Self {
let initial_cwd = std::env::current_dir().ok();
GlobalState {
shell_settings: RwLock::new(ShellSettings::new()),
active_runners: RwLock::new(HashSet::new()),
next_runner_id: std::sync::atomic::AtomicU64::new(1),
signal_handlers_installed: AtomicBool::new(false),
virtual_commands_enabled: AtomicBool::new(true),
initial_cwd: RwLock::new(initial_cwd),
}
}
pub async fn get_shell_settings(&self) -> ShellSettings {
self.shell_settings.read().await.clone()
}
pub async fn set_shell_settings(&self, settings: ShellSettings) {
*self.shell_settings.write().await = settings;
}
pub async fn with_shell_settings<F>(&self, f: F)
where
F: FnOnce(&mut ShellSettings),
{
let mut settings = self.shell_settings.write().await;
f(&mut settings);
}
pub async fn enable_shell_option(&self, option: &str) {
self.shell_settings.write().await.enable(option);
}
pub async fn disable_shell_option(&self, option: &str) {
self.shell_settings.write().await.disable(option);
}
pub async fn register_runner(&self) -> u64 {
let id = self
.next_runner_id
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.active_runners.write().await.insert(id);
trace_lazy("GlobalState", || format!("Registered runner {}", id));
id
}
pub async fn unregister_runner(&self, id: u64) {
self.active_runners.write().await.remove(&id);
trace_lazy("GlobalState", || format!("Unregistered runner {}", id));
}
pub async fn active_runner_count(&self) -> usize {
self.active_runners.read().await.len()
}
pub fn are_signal_handlers_installed(&self) -> bool {
self.signal_handlers_installed.load(Ordering::SeqCst)
}
pub fn set_signal_handlers_installed(&self, installed: bool) {
self.signal_handlers_installed
.store(installed, Ordering::SeqCst);
}
pub fn are_virtual_commands_enabled(&self) -> bool {
self.virtual_commands_enabled.load(Ordering::SeqCst)
}
pub fn enable_virtual_commands(&self) {
self.virtual_commands_enabled.store(true, Ordering::SeqCst);
trace_lazy("GlobalState", || "Virtual commands enabled".to_string());
}
pub fn disable_virtual_commands(&self) {
self.virtual_commands_enabled.store(false, Ordering::SeqCst);
trace_lazy("GlobalState", || "Virtual commands disabled".to_string());
}
pub async fn get_initial_cwd(&self) -> Option<std::path::PathBuf> {
self.initial_cwd.read().await.clone()
}
pub async fn reset(&self) {
*self.shell_settings.write().await = ShellSettings::new();
self.active_runners.write().await.clear();
self.virtual_commands_enabled.store(true, Ordering::SeqCst);
trace_lazy("GlobalState", || "Global state reset completed".to_string());
}
pub async fn restore_cwd(&self) -> std::io::Result<()> {
if let Some(ref initial) = *self.initial_cwd.read().await {
if initial.exists() {
std::env::set_current_dir(initial)?;
}
}
Ok(())
}
}
static GLOBAL_STATE: std::sync::OnceLock<Arc<GlobalState>> = std::sync::OnceLock::new();
pub fn global_state() -> Arc<GlobalState> {
GLOBAL_STATE
.get_or_init(|| Arc::new(GlobalState::new()))
.clone()
}
pub async fn reset_global_state() {
global_state().reset().await;
}
pub async fn get_shell_settings() -> ShellSettings {
global_state().get_shell_settings().await
}
pub async fn set_shell_option(option: &str) {
global_state().enable_shell_option(option).await;
}
pub async fn unset_shell_option(option: &str) {
global_state().disable_shell_option(option).await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shell_settings_default() {
let settings = ShellSettings::new();
assert!(!settings.errexit);
assert!(!settings.verbose);
assert!(!settings.xtrace);
assert!(!settings.pipefail);
assert!(!settings.nounset);
}
#[test]
fn test_shell_settings_set() {
let mut settings = ShellSettings::new();
settings.set("e", true);
assert!(settings.errexit);
settings.set("errexit", false);
assert!(!settings.errexit);
settings.set("o pipefail", true);
assert!(settings.pipefail);
}
#[tokio::test]
async fn test_global_state_runners() {
let state = GlobalState::new();
let id1 = state.register_runner().await;
let id2 = state.register_runner().await;
assert_eq!(state.active_runner_count().await, 2);
assert!(id1 != id2);
state.unregister_runner(id1).await;
assert_eq!(state.active_runner_count().await, 1);
state.unregister_runner(id2).await;
assert_eq!(state.active_runner_count().await, 0);
}
#[tokio::test]
async fn test_global_state_virtual_commands() {
let state = GlobalState::new();
assert!(state.are_virtual_commands_enabled());
state.disable_virtual_commands();
assert!(!state.are_virtual_commands_enabled());
state.enable_virtual_commands();
assert!(state.are_virtual_commands_enabled());
}
#[tokio::test]
async fn test_global_state_reset() {
let state = GlobalState::new();
state.enable_shell_option("errexit").await;
state.register_runner().await;
state.disable_virtual_commands();
state.reset().await;
let settings = state.get_shell_settings().await;
assert!(!settings.errexit);
assert_eq!(state.active_runner_count().await, 0);
assert!(state.are_virtual_commands_enabled());
}
}