Skip to main content

command_stream/
state.rs

1//! Global state management for command-stream
2//!
3//! This module handles signal handlers, process tracking, and cleanup,
4//! similar to the JavaScript $.state.mjs module.
5
6use std::collections::HashSet;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11use crate::trace::trace_lazy;
12
13/// Shell settings for controlling execution behavior
14#[derive(Debug, Clone, Default)]
15pub struct ShellSettings {
16    /// Exit immediately if a command exits with non-zero status (set -e)
17    pub errexit: bool,
18    /// Print commands as they are executed (set -v)
19    pub verbose: bool,
20    /// Print trace of commands (set -x)
21    pub xtrace: bool,
22    /// Return value of a pipeline is the status of the last command to exit with non-zero (set -o pipefail)
23    pub pipefail: bool,
24    /// Treat unset variables as an error (set -u)
25    pub nounset: bool,
26    /// Disable filename globbing (set -f)
27    pub noglob: bool,
28    /// Export all variables (set -a)
29    pub allexport: bool,
30}
31
32impl ShellSettings {
33    /// Create new shell settings with defaults
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Reset all settings to their defaults
39    pub fn reset(&mut self) {
40        *self = Self::default();
41    }
42
43    /// Set a shell option by name
44    ///
45    /// Supports both short flags (e, v, x, u, f, a) and long names
46    pub fn set(&mut self, option: &str, value: bool) {
47        match option {
48            "e" | "errexit" => self.errexit = value,
49            "v" | "verbose" => self.verbose = value,
50            "x" | "xtrace" => self.xtrace = value,
51            "u" | "nounset" => self.nounset = value,
52            "f" | "noglob" => self.noglob = value,
53            "a" | "allexport" => self.allexport = value,
54            "o pipefail" | "pipefail" => self.pipefail = value,
55            _ => {
56                trace_lazy("ShellSettings", || {
57                    format!("Unknown shell option: {}", option)
58                });
59            }
60        }
61    }
62
63    /// Enable a shell option
64    pub fn enable(&mut self, option: &str) {
65        self.set(option, true);
66    }
67
68    /// Disable a shell option
69    pub fn disable(&mut self, option: &str) {
70        self.set(option, false);
71    }
72}
73
74/// Global state for the command-stream library
75pub struct GlobalState {
76    /// Current shell settings
77    shell_settings: RwLock<ShellSettings>,
78    /// Set of active process runner IDs
79    active_runners: RwLock<HashSet<u64>>,
80    /// Counter for generating runner IDs
81    next_runner_id: std::sync::atomic::AtomicU64,
82    /// Whether signal handlers are installed
83    signal_handlers_installed: AtomicBool,
84    /// Whether virtual commands are enabled
85    virtual_commands_enabled: AtomicBool,
86    /// Initial working directory
87    initial_cwd: RwLock<Option<std::path::PathBuf>>,
88}
89
90impl Default for GlobalState {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl GlobalState {
97    /// Create a new global state
98    pub fn new() -> Self {
99        let initial_cwd = std::env::current_dir().ok();
100
101        GlobalState {
102            shell_settings: RwLock::new(ShellSettings::new()),
103            active_runners: RwLock::new(HashSet::new()),
104            next_runner_id: std::sync::atomic::AtomicU64::new(1),
105            signal_handlers_installed: AtomicBool::new(false),
106            virtual_commands_enabled: AtomicBool::new(true),
107            initial_cwd: RwLock::new(initial_cwd),
108        }
109    }
110
111    /// Get the current shell settings
112    pub async fn get_shell_settings(&self) -> ShellSettings {
113        self.shell_settings.read().await.clone()
114    }
115
116    /// Set shell settings
117    pub async fn set_shell_settings(&self, settings: ShellSettings) {
118        *self.shell_settings.write().await = settings;
119    }
120
121    /// Modify shell settings with a closure
122    pub async fn with_shell_settings<F>(&self, f: F)
123    where
124        F: FnOnce(&mut ShellSettings),
125    {
126        let mut settings = self.shell_settings.write().await;
127        f(&mut settings);
128    }
129
130    /// Enable a shell option
131    pub async fn enable_shell_option(&self, option: &str) {
132        self.shell_settings.write().await.enable(option);
133    }
134
135    /// Disable a shell option
136    pub async fn disable_shell_option(&self, option: &str) {
137        self.shell_settings.write().await.disable(option);
138    }
139
140    /// Register a new active runner and return its ID
141    pub async fn register_runner(&self) -> u64 {
142        let id = self
143            .next_runner_id
144            .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
145        self.active_runners.write().await.insert(id);
146
147        trace_lazy("GlobalState", || format!("Registered runner {}", id));
148
149        id
150    }
151
152    /// Unregister an active runner
153    pub async fn unregister_runner(&self, id: u64) {
154        self.active_runners.write().await.remove(&id);
155
156        trace_lazy("GlobalState", || format!("Unregistered runner {}", id));
157    }
158
159    /// Get the count of active runners
160    pub async fn active_runner_count(&self) -> usize {
161        self.active_runners.read().await.len()
162    }
163
164    /// Check if signal handlers are installed
165    pub fn are_signal_handlers_installed(&self) -> bool {
166        self.signal_handlers_installed.load(Ordering::SeqCst)
167    }
168
169    /// Mark signal handlers as installed
170    pub fn set_signal_handlers_installed(&self, installed: bool) {
171        self.signal_handlers_installed
172            .store(installed, Ordering::SeqCst);
173    }
174
175    /// Check if virtual commands are enabled
176    pub fn are_virtual_commands_enabled(&self) -> bool {
177        self.virtual_commands_enabled.load(Ordering::SeqCst)
178    }
179
180    /// Enable virtual commands
181    pub fn enable_virtual_commands(&self) {
182        self.virtual_commands_enabled.store(true, Ordering::SeqCst);
183        trace_lazy("GlobalState", || "Virtual commands enabled".to_string());
184    }
185
186    /// Disable virtual commands
187    pub fn disable_virtual_commands(&self) {
188        self.virtual_commands_enabled.store(false, Ordering::SeqCst);
189        trace_lazy("GlobalState", || "Virtual commands disabled".to_string());
190    }
191
192    /// Get the initial working directory
193    pub async fn get_initial_cwd(&self) -> Option<std::path::PathBuf> {
194        self.initial_cwd.read().await.clone()
195    }
196
197    /// Reset global state to defaults
198    pub async fn reset(&self) {
199        // Reset shell settings
200        *self.shell_settings.write().await = ShellSettings::new();
201
202        // Clear active runners
203        self.active_runners.write().await.clear();
204
205        // Reset virtual commands flag
206        self.virtual_commands_enabled.store(true, Ordering::SeqCst);
207
208        // Don't reset signal handlers installed flag - that's managed separately
209
210        trace_lazy("GlobalState", || "Global state reset completed".to_string());
211    }
212
213    /// Restore working directory to initial
214    pub async fn restore_cwd(&self) -> std::io::Result<()> {
215        if let Some(ref initial) = *self.initial_cwd.read().await {
216            if initial.exists() {
217                std::env::set_current_dir(initial)?;
218            }
219        }
220        Ok(())
221    }
222}
223
224/// Global state singleton
225static GLOBAL_STATE: std::sync::OnceLock<Arc<GlobalState>> = std::sync::OnceLock::new();
226
227/// Get the global state instance
228pub fn global_state() -> Arc<GlobalState> {
229    GLOBAL_STATE
230        .get_or_init(|| Arc::new(GlobalState::new()))
231        .clone()
232}
233
234/// Reset the global state (for testing)
235pub async fn reset_global_state() {
236    global_state().reset().await;
237}
238
239/// Get current shell settings
240pub async fn get_shell_settings() -> ShellSettings {
241    global_state().get_shell_settings().await
242}
243
244/// Enable a shell option globally
245pub async fn set_shell_option(option: &str) {
246    global_state().enable_shell_option(option).await;
247}
248
249/// Disable a shell option globally
250pub async fn unset_shell_option(option: &str) {
251    global_state().disable_shell_option(option).await;
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_shell_settings_default() {
260        let settings = ShellSettings::new();
261        assert!(!settings.errexit);
262        assert!(!settings.verbose);
263        assert!(!settings.xtrace);
264        assert!(!settings.pipefail);
265        assert!(!settings.nounset);
266    }
267
268    #[test]
269    fn test_shell_settings_set() {
270        let mut settings = ShellSettings::new();
271
272        settings.set("e", true);
273        assert!(settings.errexit);
274
275        settings.set("errexit", false);
276        assert!(!settings.errexit);
277
278        settings.set("o pipefail", true);
279        assert!(settings.pipefail);
280    }
281
282    #[tokio::test]
283    async fn test_global_state_runners() {
284        let state = GlobalState::new();
285
286        let id1 = state.register_runner().await;
287        let id2 = state.register_runner().await;
288
289        assert_eq!(state.active_runner_count().await, 2);
290        assert!(id1 != id2);
291
292        state.unregister_runner(id1).await;
293        assert_eq!(state.active_runner_count().await, 1);
294
295        state.unregister_runner(id2).await;
296        assert_eq!(state.active_runner_count().await, 0);
297    }
298
299    #[tokio::test]
300    async fn test_global_state_virtual_commands() {
301        let state = GlobalState::new();
302
303        assert!(state.are_virtual_commands_enabled());
304
305        state.disable_virtual_commands();
306        assert!(!state.are_virtual_commands_enabled());
307
308        state.enable_virtual_commands();
309        assert!(state.are_virtual_commands_enabled());
310    }
311
312    #[tokio::test]
313    async fn test_global_state_reset() {
314        let state = GlobalState::new();
315
316        // Modify state
317        state.enable_shell_option("errexit").await;
318        state.register_runner().await;
319        state.disable_virtual_commands();
320
321        // Reset
322        state.reset().await;
323
324        // Verify reset
325        let settings = state.get_shell_settings().await;
326        assert!(!settings.errexit);
327        assert_eq!(state.active_runner_count().await, 0);
328        assert!(state.are_virtual_commands_enabled());
329    }
330}