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
This commit is contained in:
Ellie Huxtable 2024-02-29 15:32:48 +00:00 committed by GitHub
parent 5f0e6dd307
commit 6d62749e19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 290 additions and 0 deletions

85
Cargo.lock generated
View File

@ -212,6 +212,8 @@ dependencies = [
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml",
"sysinfo",
"time", "time",
"tiny-bip39", "tiny-bip39",
"tokio", "tokio",
@ -804,6 +806,16 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 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]] [[package]]
name = "crossbeam-epoch" name = "crossbeam-epoch"
version = "0.9.18" version = "0.9.18"
@ -2108,6 +2120,15 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "ntapi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.46.0"
@ -2617,6 +2638,26 @@ dependencies = [
"bitflags 1.3.2", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.4.1"
@ -3100,6 +3141,19 @@ dependencies = [
"syn 2.0.51", "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]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"
@ -3560,6 +3614,21 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 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]] [[package]]
name = "system-configuration" name = "system-configuration"
version = "0.5.1" version = "0.5.1"
@ -3976,6 +4045,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "unsafe-libyaml"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@ -4231,6 +4306,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.52.0" version = "0.52.0"

View File

@ -80,6 +80,8 @@ tracing = "0.1"
cli-clipboard = { version = "0.4.0", optional = true } cli-clipboard = { version = "0.4.0", optional = true }
uuid = { workspace = true } uuid = { workspace = true }
unicode-segmentation = "1.11.0" unicode-segmentation = "1.11.0"
serde_yaml = "0.9.32"
sysinfo = "0.30.5"
[dependencies.tracing-subscriber] [dependencies.tracing-subscriber]

View File

@ -14,6 +14,7 @@ mod account;
mod config; mod config;
mod default_config; mod default_config;
mod doctor;
mod history; mod history;
mod import; mod import;
mod init; mod init;
@ -58,6 +59,9 @@ pub enum Cmd {
#[command()] #[command()]
Init(init::Cmd), Init(init::Cmd),
#[command()]
Doctor,
/// Print example configuration /// Print example configuration
#[command()] #[command()]
DefaultConfig, DefaultConfig,
@ -113,6 +117,8 @@ impl Cmd {
Self::Init(init) => init.run(&settings).await, Self::Init(init) => init.run(&settings).await,
Self::Doctor => doctor::run(&settings),
Self::DefaultConfig => { Self::DefaultConfig => {
default_config::run(); default_config::run();
Ok(()) Ok(())

View File

@ -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<String>,
}
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<String> {
// 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<DiskInfo>,
}
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<SyncInfo>,
}
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(())
}