diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-06-12 01:54:21 +0200 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-06-12 01:54:21 +0200 |
| commit | bbdf38018b47328b5faa2cef635c37095045be72 (patch) | |
| tree | 8983817d547551ae12508a8ae8731b622d990af4 /crates/turtle/src/command/client/doctor.rs | |
| parent | feat(server): Make user stuff stateless (diff) | |
| download | atuin-bbdf38018b47328b5faa2cef635c37095045be72.zip | |
feat(server): Really make users stateless (with tests)
This commit also remove another load of unneeded features.
Diffstat (limited to 'crates/turtle/src/command/client/doctor.rs')
| -rw-r--r-- | crates/turtle/src/command/client/doctor.rs | 405 |
1 files changed, 0 insertions, 405 deletions
diff --git a/crates/turtle/src/command/client/doctor.rs b/crates/turtle/src/command/client/doctor.rs deleted file mode 100644 index eec690a5..00000000 --- a/crates/turtle/src/command/client/doctor.rs +++ /dev/null @@ -1,405 +0,0 @@ -use std::process::Command; -use std::{env, str::FromStr}; - -use crate::atuin_client::database::Sqlite; -use crate::atuin_client::settings::Settings; -use crate::atuin_common::shell::{Shell, shell_name}; -use crate::atuin_common::utils; -use colored::Colorize; -use eyre::Result; -use serde::Serialize; - -use sysinfo::{Disks, System, get_current_pid}; - -#[derive(Debug, Serialize)] -struct ShellInfo { - pub(crate) name: String, - - // best-effort, not supported on all OSes - pub(crate) default: String, - - // Detect some shell plugins that the user has installed. - // I'm just going to start with preexec/blesh - pub(crate) plugins: Vec<String>, - - // The preexec framework used in the current session, if Atuin is loaded. - pub(crate) preexec: Option<String>, -} - -impl ShellInfo { - // HACK ALERT! - // Many of the shell vars we need to detect are not exported :( - // So, we're going to run a interactive session and directly check the - // variable. There's a chance this won't work, so it should not be fatal. - // - // Every shell we support handles `shell -ic 'command'` - fn shellvar_exists(shell: &str, var: &str) -> bool { - let cmd = Command::new(shell) - .args([ - "-ic", - format!("[ -z ${var} ] || echo ATUIN_DOCTOR_ENV_FOUND").as_str(), - ]) - .output() - .map_or(String::new(), |v| { - let out = v.stdout; - String::from_utf8(out).unwrap_or_default() - }); - - cmd.contains("ATUIN_DOCTOR_ENV_FOUND") - } - - fn detect_preexec_framework(shell: &str) -> Option<String> { - if env::var("ATUIN_SESSION").ok().is_none() { - None - } else if shell.starts_with("bash") || shell == "sh" { - env::var("ATUIN_PREEXEC_BACKEND") - .ok() - .filter(|value| !value.is_empty()) - .and_then(|atuin_preexec_backend| { - atuin_preexec_backend.rfind(':').and_then(|pos_colon| { - u32::from_str(&atuin_preexec_backend[..pos_colon]) - .ok() - .is_some_and(|preexec_shlvl| { - env::var("SHLVL") - .ok() - .and_then(|shlvl| u32::from_str(&shlvl).ok()) - .is_some_and(|shlvl| shlvl == preexec_shlvl) - }) - .then(|| atuin_preexec_backend[pos_colon + 1..].to_string()) - }) - }) - } else { - Some("built-in".to_string()) - } - } - - fn validate_plugin_blesh( - _shell: &str, - shell_process: &sysinfo::Process, - ble_session_id: &str, - ) -> Option<String> { - ble_session_id - .split('/') - .nth(1) - .and_then(|field| u32::from_str(field).ok()) - .filter(|&blesh_pid| blesh_pid == shell_process.pid().as_u32()) - .map(|_| "blesh".to_string()) - } - - pub(crate) fn plugins(shell: &str, shell_process: &sysinfo::Process) -> Vec<String> { - // consider a different detection approach if there are plugins - // that don't set shell vars - - enum PluginShellType { - Any, - Bash, - - // Note: these are currently unused - #[expect(dead_code)] - Zsh, - #[expect(dead_code)] - Fish, - #[expect(dead_code)] - Nushell, - #[expect(dead_code)] - Xonsh, - } - - enum PluginProbeType { - EnvironmentVariable(&'static str), - InteractiveShellVariable(&'static str), - } - - type PluginValidator = fn(&str, &sysinfo::Process, &str) -> Option<String>; - - let plugin_list: [( - &str, - PluginShellType, - PluginProbeType, - Option<PluginValidator>, - ); 3] = [ - ( - "atuin", - PluginShellType::Any, - PluginProbeType::EnvironmentVariable("ATUIN_SESSION"), - None, - ), - ( - "blesh", - PluginShellType::Bash, - PluginProbeType::EnvironmentVariable("BLE_SESSION_ID"), - Some(Self::validate_plugin_blesh), - ), - ( - "bash-preexec", - PluginShellType::Bash, - PluginProbeType::InteractiveShellVariable("bash_preexec_imported"), - None, - ), - ]; - - plugin_list - .into_iter() - .filter(|(_, shell_type, _, _)| match shell_type { - PluginShellType::Any => true, - PluginShellType::Bash => shell.starts_with("bash") || shell == "sh", - PluginShellType::Zsh => shell.starts_with("zsh"), - PluginShellType::Fish => shell.starts_with("fish"), - PluginShellType::Nushell => shell.starts_with("nu"), - PluginShellType::Xonsh => shell.starts_with("xonsh"), - }) - .filter_map(|(plugin, _, probe_type, validator)| -> Option<String> { - match probe_type { - PluginProbeType::EnvironmentVariable(env) => { - env::var(env).ok().filter(|value| !value.is_empty()) - } - PluginProbeType::InteractiveShellVariable(shellvar) => { - ShellInfo::shellvar_exists(shell, shellvar).then_some(String::default()) - } - } - .and_then(|value| { - validator.map_or_else( - || Some(plugin.to_string()), - |validator| validator(shell, shell_process, &value), - ) - }) - }) - .collect() - } - - pub(crate) fn new() -> Self { - // TODO: rework to use crate::atuin_common::Shell - - let sys = System::new_all(); - - let process = sys - .process(get_current_pid().expect("Failed to get current PID")) - .expect("Process with current pid does not exist"); - - let parent = sys - .process(process.parent().expect("Atuin running with no parent!")) - .expect("Process with parent pid does not exist"); - - let name = shell_name(Some(parent)); - - let plugins = ShellInfo::plugins(name.as_str(), parent); - - let default = Shell::default_shell().unwrap_or(Shell::Unknown).to_string(); - - let preexec = Self::detect_preexec_framework(name.as_str()); - - Self { - name, - default, - plugins, - preexec, - } - } -} - -#[derive(Debug, Serialize)] -struct DiskInfo { - pub(crate) name: String, - pub(crate) filesystem: String, -} - -#[derive(Debug, Serialize)] -struct SystemInfo { - pub(crate) os: String, - - pub(crate) arch: String, - - pub(crate) version: String, - pub(crate) disks: Vec<DiskInfo>, -} - -impl SystemInfo { - pub(crate) fn new() -> Self { - let disks = Disks::new_with_refreshed_list(); - let disks = disks - .list() - .iter() - .map(|d| DiskInfo { - name: d.name().to_os_string().into_string().unwrap(), - filesystem: d.file_system().to_os_string().into_string().unwrap(), - }) - .collect(); - - Self { - os: System::name().unwrap_or_else(|| "unknown".to_string()), - arch: System::cpu_arch().unwrap_or_else(|| "unknown".to_string()), - version: System::os_version().unwrap_or_else(|| "unknown".to_string()), - disks, - } - } -} - -#[derive(Debug, Serialize)] -struct SyncInfo { - pub(crate) auth_state: String, - pub(crate) auto_sync: bool, - - pub(crate) last_sync: String, -} - -impl SyncInfo { - pub(crate) async fn new(settings: &Settings) -> Result<Self> { - let has_cli_token = settings.have_sync_key().await?; - - let auth_state = if has_cli_token { - "Self-hosted (authenticated)".into() - } else { - "Not authenticated".into() - }; - - Ok(Self { - auth_state, - auto_sync: settings.auto_sync, - last_sync: Settings::last_sync() - .await - .map_or_else(|_| "no last sync".to_string(), |v| v.to_string()), - }) - } -} - -#[derive(Debug)] -struct SettingPaths { - db: String, - record_store: String, - key: String, -} - -impl SettingPaths { - pub(crate) fn new(settings: &Settings) -> Self { - Self { - db: settings.db_path.clone(), - record_store: settings.record_store_path.clone(), - key: settings.key_path.clone(), - } - } - - pub(crate) fn verify(&self) { - let paths = vec![ - ("ATUIN_DB_PATH", &self.db), - ("ATUIN_RECORD_STORE", &self.record_store), - ("ATUIN_KEY", &self.key), - ]; - - for (path_env_var, path) in paths { - if utils::broken_symlink(path) { - eprintln!( - "{path} (${path_env_var}) is a broken symlink. This may cause issues with Atuin." - ); - } - } - } -} - -#[derive(Debug, Serialize)] -struct AtuinInfo { - pub(crate) version: String, - pub(crate) commit: String, - - /// Whether the main Atuin sync server is in use - /// I'm just calling it Atuin Cloud for lack of a better name atm - pub(crate) sync: Option<SyncInfo>, - - pub(crate) sqlite_version: String, - - #[serde(skip)] // probably unnecessary to expose this - pub(crate) setting_paths: SettingPaths, -} - -impl AtuinInfo { - pub(crate) async fn new(settings: &Settings) -> Result<Self> { - let logged_in = settings.have_sync_key().await?; - - let sync = if logged_in { - Some(SyncInfo::new(settings).await?) - } else { - None - }; - - let sqlite_version = match Sqlite::new("sqlite::memory:", 0.1).await { - Ok(db) => db - .sqlite_version() - .await - .unwrap_or_else(|_| "unknown".to_string()), - Err(_) => "error".to_string(), - }; - - Ok(Self { - version: crate::VERSION.to_string(), - commit: crate::SHA.to_string(), - sync, - sqlite_version, - setting_paths: SettingPaths::new(settings), - }) - } -} - -#[derive(Debug, Serialize)] -struct DoctorDump { - pub(crate) atuin: AtuinInfo, - pub(crate) shell: ShellInfo, - pub(crate) system: SystemInfo, -} - -impl DoctorDump { - pub(crate) async fn new(settings: &Settings) -> Result<Self> { - Ok(Self { - atuin: AtuinInfo::new(settings).await?, - shell: ShellInfo::new(), - system: SystemInfo::new(), - }) - } -} - -fn checks(info: &DoctorDump) { - println!(); // spacing - // - let zfs_error = "[Filesystem] ZFS is known to have some issues with SQLite. Atuin uses SQLite heavily. If you are having poor performance, there are some workarounds here: https://github.com/atuinsh/atuin/issues/952".bold().red(); - let bash_plugin_error = "[Shell] If you are using Bash, Atuin requires that either bash-preexec or ble.sh (>= 0.4) be installed. An older ble.sh may not be detected. so ignore this if you have ble.sh >= 0.4 set up! Read more here: https://docs.atuin.sh/guide/installation/#bash".bold().red(); - let blesh_integration_error = "[Shell] Atuin and ble.sh seem to be loaded in the session, but the integration does not seem to be working. Please check the setup in .bashrc.".bold().red(); - - // ZFS: https://github.com/atuinsh/atuin/issues/952 - if info.system.disks.iter().any(|d| d.filesystem == "zfs") { - println!("{zfs_error}"); - } - - info.atuin.setting_paths.verify(); - - // Shell - if info.shell.name == "bash" { - if !info - .shell - .plugins - .iter() - .any(|p| p == "blesh" || p == "bash-preexec") - { - println!("{bash_plugin_error}"); - } - - if info.shell.plugins.iter().any(|plugin| plugin == "atuin") - && info.shell.plugins.iter().any(|plugin| plugin == "blesh") - && info.shell.preexec.as_ref().is_some_and(|val| val == "none") - { - println!("{blesh_integration_error}"); - } - } -} - -pub(crate) async fn run(settings: &Settings) -> Result<()> { - println!("{}", "Atuin Doctor".bold()); - println!("Checking for diagnostics"); - let dump = DoctorDump::new(settings).await?; - - checks(&dump); - - let dump = serde_json::to_string_pretty(&dump)?; - - println!("\nPlease include the output below with any bug reports or issues\n"); - println!("{dump}"); - - Ok(()) -} |
