aboutsummaryrefslogtreecommitdiffstats
path: root/crates/turtle/src/command/client/doctor.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/turtle/src/command/client/doctor.rs')
-rw-r--r--crates/turtle/src/command/client/doctor.rs405
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(())
-}