From 6d62749e199fe1ee164e6d8e66c2e53c08f8db42 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Thu, 29 Feb 2024 15:32:48 +0000 Subject: feat: add atuin doctor (#1796) * feat add atuin doctor * registered -> logged_in * not logged in, no sync info * add plugin detection * add a hack * clippy * add filesystem detection * add title * hmm * need interactive shell --- Cargo.lock | 85 ++++++++++++++++ atuin/Cargo.toml | 2 + atuin/src/command/client.rs | 6 ++ atuin/src/command/client/doctor.rs | 197 +++++++++++++++++++++++++++++++++++++ 4 files changed, 290 insertions(+) create mode 100644 atuin/src/command/client/doctor.rs diff --git a/Cargo.lock b/Cargo.lock index 338dab76..ab0eaa31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,8 @@ dependencies = [ "semver", "serde", "serde_json", + "serde_yaml", + "sysinfo", "time", "tiny-bip39", "tokio", @@ -804,6 +806,16 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-epoch" version = "0.9.18" @@ -2108,6 +2120,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2617,6 +2638,26 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "rayon" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -3100,6 +3141,19 @@ dependencies = [ "syn 2.0.51", ] +[[package]] +name = "serde_yaml" +version = "0.9.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f" +dependencies = [ + "indexmap 2.2.3", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3560,6 +3614,21 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sysinfo" +version = "0.30.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb4f3438c8f6389c864e61221cbc97e9bca98b4daf39a5beb7bea660f528bb2" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -3976,6 +4045,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" + [[package]] name = "untrusted" version = "0.9.0" @@ -4231,6 +4306,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.3", +] + [[package]] name = "windows-core" version = "0.52.0" diff --git a/atuin/Cargo.toml b/atuin/Cargo.toml index 21318431..b53a8b5b 100644 --- a/atuin/Cargo.toml +++ b/atuin/Cargo.toml @@ -80,6 +80,8 @@ tracing = "0.1" cli-clipboard = { version = "0.4.0", optional = true } uuid = { workspace = true } unicode-segmentation = "1.11.0" +serde_yaml = "0.9.32" +sysinfo = "0.30.5" [dependencies.tracing-subscriber] diff --git a/atuin/src/command/client.rs b/atuin/src/command/client.rs index b74c9bc7..5b66aa39 100644 --- a/atuin/src/command/client.rs +++ b/atuin/src/command/client.rs @@ -14,6 +14,7 @@ mod account; mod config; mod default_config; +mod doctor; mod history; mod import; mod init; @@ -58,6 +59,9 @@ pub enum Cmd { #[command()] Init(init::Cmd), + #[command()] + Doctor, + /// Print example configuration #[command()] DefaultConfig, @@ -113,6 +117,8 @@ impl Cmd { Self::Init(init) => init.run(&settings).await, + Self::Doctor => doctor::run(&settings), + Self::DefaultConfig => { default_config::run(); Ok(()) diff --git a/atuin/src/command/client/doctor.rs b/atuin/src/command/client/doctor.rs new file mode 100644 index 00000000..6602c04f --- /dev/null +++ b/atuin/src/command/client/doctor.rs @@ -0,0 +1,197 @@ +use std::process::Command; +use std::{collections::HashMap, path::PathBuf}; + +use atuin_client::settings::Settings; +use colored::Colorize; +use eyre::Result; +use serde::{Deserialize, Serialize}; + +use sysinfo::{get_current_pid, Disks, System}; + +#[derive(Debug, Serialize, Deserialize)] +struct ShellInfo { + pub name: String, + + // Detect some shell plugins that the user has installed. + // I'm just going to start with preexec/blesh + pub plugins: Vec, +} + +impl ShellInfo { + // HACK ALERT! + // Many of the env vars we need to detect are not exported :( + // So, we're going to run `env` in a subshell and parse the output + // There's a chance this won't work, so it should not be fatal. + // + // Every shell we support handles `shell -c 'command'` + fn env_exists(shell: &str, var: &str) -> bool { + let mut cmd = Command::new(shell) + .args(["-ic", format!("echo ${var}").as_str()]) + .output() + .map_or(String::new(), |v| { + let out = v.stdout; + String::from_utf8(out).unwrap_or_default() + }); + + cmd.retain(|c| !c.is_whitespace()); + + !cmd.is_empty() + } + + pub fn plugins(shell: &str) -> Vec { + // consider a different detection approach if there are plugins + // that don't set env vars + + let map = HashMap::from([ + ("ATUIN_SESSION", "atuin"), + ("BLE_ATTACHED", "blesh"), + ("bash_preexec_imported", "bash-preexec"), + ]); + + map.into_iter() + .filter_map(|(env, plugin)| { + if ShellInfo::env_exists(shell, env) { + return Some(plugin.to_string()); + } + + None + }) + .collect() + } + + pub fn new() -> Self { + 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 shell = parent.name().trim().to_lowercase(); + let shell = shell.strip_prefix('-').unwrap_or(&shell); + let name = shell.to_string(); + + let plugins = ShellInfo::plugins(name.as_str()); + + Self { name, plugins } + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct DiskInfo { + pub name: String, + pub filesystem: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct SystemInfo { + pub os: String, + + pub arch: String, + + pub version: String, + pub disks: Vec, +} + +impl SystemInfo { + pub 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, Deserialize)] +struct SyncInfo { + /// Whether the main Atuin sync server is in use + /// I'm just calling it Atuin Cloud for lack of a better name atm + pub cloud: bool, + pub records: bool, + pub auto_sync: bool, + + pub last_sync: String, +} + +impl SyncInfo { + pub fn new(settings: &Settings) -> Self { + Self { + cloud: settings.sync_address == "https://api.atuin.sh", + auto_sync: settings.auto_sync, + records: settings.sync.records, + last_sync: Settings::last_sync().map_or("no last sync".to_string(), |v| v.to_string()), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct AtuinInfo { + pub version: 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 sync: Option, +} + +impl AtuinInfo { + pub fn new(settings: &Settings) -> Self { + let session_path = settings.session_path.as_str(); + let logged_in = PathBuf::from(session_path).exists(); + + let sync = if logged_in { + Some(SyncInfo::new(settings)) + } else { + None + }; + + Self { + version: crate::VERSION.to_string(), + sync, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct DoctorDump { + pub atuin: AtuinInfo, + pub shell: ShellInfo, + pub system: SystemInfo, +} + +impl DoctorDump { + pub fn new(settings: &Settings) -> Self { + Self { + atuin: AtuinInfo::new(settings), + shell: ShellInfo::new(), + system: SystemInfo::new(), + } + } +} + +pub fn run(settings: &Settings) -> Result<()> { + println!("{}", "Atuin Doctor".bold()); + println!("Checking for diagnostics"); + println!("Please include the output below with any bug reports or issues\n"); + + let dump = DoctorDump::new(settings); + + let dump = serde_yaml::to_string(&dump)?; + println!("{dump}"); + + Ok(()) +} -- cgit v1.2.3