Skip to main content

rusty_stack/
lib.rs

1//! Rusty-Stack Library
2//!
3//! This library contains the core TUI installer, benchmark infrastructure,
4//! and the new platform modules (core, platform, orchestrator, telemetry,
5//! adapter).
6
7pub mod adapter;
8pub mod benchmark_logs;
9pub mod benchmark_runners;
10pub mod benchmarks;
11pub mod bootstrap;
12pub mod component_status;
13pub mod config;
14pub mod core;
15pub mod gpu;
16pub mod hardware;
17pub mod installer;
18pub mod installers;
19pub mod logging;
20pub mod orchestrator;
21pub mod platform;
22pub mod state;
23pub mod telemetry;
24pub mod uninstall;
25pub mod verification;
26
27/// Shared test infrastructure.
28///
29/// `env_lock()` is a SINGLE global mutex acquired by every test that mutates a
30/// process-global env var (MLSTACK_USER_HOME, MLSTACK_PYTHON_BIN, ROCM_PATH,
31/// package-manager overrides, DRY_RUN, GITHUB_TOKEN, …). Process env is global
32/// state, so any two env-mutating tests running concurrently on different
33/// threads corrupt each other — serializing ALL of them through this one lock
34/// makes the suite deterministic under `cargo test`'s default thread pool.
35#[cfg(test)]
36pub mod test_support {
37    use std::sync::{Mutex, MutexGuard, OnceLock};
38
39    static GLOBAL_ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
40
41    /// The single global mutex serializing all env-mutating / global-state tests.
42    pub fn env_lock() -> &'static Mutex<()> {
43        GLOBAL_ENV_LOCK.get_or_init(|| Mutex::new(()))
44    }
45
46    /// Acquire at the top of any test that mutates process-global state (env
47    /// vars, the /tmp/llama-cpp-rocm-build dir, …). Returns a guard that serializes
48    /// the test against every other such test.
49    ///
50    /// Recovers from **poison**: if a previous test panicked while holding the
51    /// lock, the mutex is poisoned and a plain `.lock().unwrap()` would cascade
52    /// the panic into every other locked test. We ignore the poison so each test
53    /// fails (or passes) on its own merits, not as collateral damage.
54    pub fn lock_env() -> MutexGuard<'static, ()> {
55        env_lock()
56            .lock()
57            .unwrap_or_else(|poison| poison.into_inner())
58    }
59}
60
61#[cfg(feature = "tui")]
62pub mod app;
63
64#[cfg(feature = "tui")]
65pub mod widgets;
66
67use std::path::PathBuf;
68
69/// Detect the scripts directory by checking current dir and parent dir.
70pub fn detect_scripts_dir() -> String {
71    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
72    let scripts = cwd.join("scripts");
73    if scripts.exists() {
74        return scripts.to_string_lossy().to_string();
75    }
76    let parent_scripts = cwd.join("..").join("scripts");
77    if parent_scripts.exists() {
78        return parent_scripts.to_string_lossy().to_string();
79    }
80    "./scripts".to_string()
81}
82
83/// Set the MLSTACK_REPO_ROOT environment variable based on scripts dir location.
84pub fn set_repo_root(scripts_dir: &str) {
85    let scripts_path = PathBuf::from(scripts_dir);
86    if let Some(root) = scripts_path.parent() {
87        if root.join("stans_ml_stack").exists() {
88            std::env::set_var("MLSTACK_REPO_ROOT", root);
89        }
90    }
91}
92
93/// Run the TUI application.
94///
95/// This function encapsulates the full TUI lifecycle: panic hook setup,
96/// raw mode, alternate screen, terminal creation, app initialization,
97/// event loop, and cleanup.
98#[cfg(feature = "tui")]
99pub fn run_tui() -> anyhow::Result<()> {
100    use crate::app::App;
101    use crossterm::execute;
102    use crossterm::terminal::{
103        disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
104    };
105    use ratatui::backend::CrosstermBackend;
106    use ratatui::Terminal;
107    use std::io;
108    use std::time::{Duration, Instant};
109
110    std::panic::set_hook(Box::new(|info| {
111        let _ = disable_raw_mode();
112        let mut stdout = io::stdout();
113        let _ = execute!(stdout, LeaveAlternateScreen, crossterm::cursor::Show);
114        eprintln!("Rusty-Stack crashed: {info}");
115    }));
116
117    enable_raw_mode()?;
118    let mut stdout = io::stdout();
119    let use_alt_screen = std::env::var("MLSTACK_NO_ALT_SCREEN").is_err();
120    if use_alt_screen {
121        execute!(stdout, EnterAlternateScreen)?;
122    }
123    let backend = CrosstermBackend::new(stdout);
124    let mut terminal = Terminal::new(backend)?;
125    // Hide the cursor for the duration of the TUI — it is never positioned
126    // (no text-input cursor), so a visible cursor floats to the last-redrawn
127    // cell each frame and shows up as a stray character. Restored on exit.
128    //
129    // If this fails, clean up the terminal state we already enabled (raw mode +
130    // alternate screen) before propagating the error. The global panic hook
131    // (above) only fires on unwinding, not on an early `?` return, so without
132    // this cleanup a hide_cursor failure would leave the user's terminal in raw
133    // mode inside the alternate screen — visibly broken.
134    if let Err(e) = terminal.hide_cursor() {
135        let _ = disable_raw_mode();
136        if use_alt_screen {
137            let _ = execute!(terminal.backend_mut(), LeaveAlternateScreen);
138        }
139        return Err(e.into());
140    }
141
142    let scripts_dir = detect_scripts_dir();
143    set_repo_root(&scripts_dir);
144    let mut app = App::new(scripts_dir);
145
146    let tick_rate = Duration::from_millis(100);
147    let mut last_tick = Instant::now();
148
149    let res = run_app(&mut terminal, &mut app, tick_rate, &mut last_tick);
150
151    disable_raw_mode()?;
152    if use_alt_screen {
153        execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
154    }
155    terminal.show_cursor()?;
156
157    if let Err(err) = res {
158        eprintln!("Error: {err:?}");
159    }
160
161    Ok(())
162}
163
164/// Internal event loop for the TUI application.
165#[cfg(feature = "tui")]
166fn run_app<B: ratatui::backend::Backend>(
167    terminal: &mut ratatui::Terminal<B>,
168    app: &mut crate::app::App,
169    tick_rate: std::time::Duration,
170    last_tick: &mut std::time::Instant,
171) -> anyhow::Result<()>
172where
173    <B as ratatui::backend::Backend>::Error: Send + Sync + 'static,
174{
175    use crate::state::Stage;
176    use crossterm::event::{self, Event, KeyCode, KeyModifiers};
177
178    loop {
179        terminal.draw(|frame| app.draw(frame))?;
180
181        let timeout = tick_rate
182            .checked_sub(last_tick.elapsed())
183            .unwrap_or_else(|| std::time::Duration::from_secs(0));
184
185        if event::poll(timeout)? {
186            match event::read()? {
187                Event::Key(key) => {
188                    if key.code == KeyCode::Char('c')
189                        && key.modifiers.contains(KeyModifiers::CONTROL)
190                    {
191                        return Ok(());
192                    }
193                    if key.code == KeyCode::Char('q') && app.stage == Stage::Recovery {
194                        return Ok(());
195                    }
196                    app.handle_key(key);
197                }
198                Event::Resize(_, _) => {
199                    terminal.autoresize()?;
200                }
201                _ => {}
202            }
203        }
204
205        if last_tick.elapsed() >= tick_rate {
206            app.on_tick();
207            *last_tick = std::time::Instant::now();
208        }
209
210        if app.should_exit {
211            return Ok(());
212        }
213    }
214}