Skip to main content

radicle_cli/commands/
debug.rs

1mod args;
2
3use std::collections::BTreeMap;
4use std::env;
5use std::path::PathBuf;
6use std::process::Command;
7
8use anyhow::anyhow;
9use serde::Serialize;
10
11use radicle::Profile;
12
13use crate::terminal as term;
14
15pub use args::Args;
16
17pub const NAME: &str = "rad";
18pub const VERSION: &str = env!("RADICLE_VERSION");
19pub const DESCRIPTION: &str = "Radicle command line interface";
20pub const GIT_HEAD: &str = env!("GIT_HEAD");
21
22pub fn run(_args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
23    match ctx.profile() {
24        Ok(profile) => debug(Some(&profile)),
25        Err(e) => {
26            eprintln!("ERROR: Could not load Radicle profile: {e}");
27            debug(None)
28        }
29    }
30}
31
32// Collect information about the local Radicle installation and write
33// it out.
34fn debug(profile: Option<&Profile>) -> anyhow::Result<()> {
35    let env = BTreeMap::from_iter(env::vars().filter_map(|(k, v)| {
36        if k == "RAD_PASSPHRASE" {
37            Some((k, "<REDACTED>".into()))
38        } else if k.starts_with("RAD_") || k.starts_with("SSH_") || k == "PATH" || k == "SHELL" {
39            Some((k, v))
40        } else {
41            None
42        }
43    }));
44
45    let debug = DebugInfo {
46        rad_exe: std::env::current_exe().ok(),
47        rad_version: VERSION,
48        radicle_node_version: stdout_of("radicle-node", &["--version"])
49            .unwrap_or("radicle-node <unknown>".into()),
50        git_remote_rad_version: stdout_of("git-remote-rad", &["--version"])
51            .unwrap_or("git-remote-rad <unknown>".into()),
52        git_version: stdout_of("git", &["--version"]).unwrap_or("<unknown>".into()),
53        ssh_version: stderr_of("ssh", &["-V"]).unwrap_or("<unknown>".into()),
54        git_head: GIT_HEAD,
55        log: profile.map(|p| LogFile::new(p.node().join("node.log"))),
56        old_log: profile.map(|p| LogFile::new(p.node().join("node.log.old"))),
57        operating_system: std::env::consts::OS,
58        arch: std::env::consts::ARCH,
59        env,
60        warnings: collect_warnings(profile),
61    };
62
63    println!("{}", serde_json::to_string_pretty(&debug).unwrap());
64
65    Ok(())
66}
67
68#[derive(Debug, Serialize)]
69#[allow(dead_code)]
70#[serde(rename_all = "camelCase")]
71struct DebugInfo {
72    rad_exe: Option<PathBuf>,
73    rad_version: &'static str,
74    radicle_node_version: String,
75    git_remote_rad_version: String,
76    git_version: String,
77    ssh_version: String,
78    git_head: &'static str,
79    log: Option<LogFile>,
80    old_log: Option<LogFile>,
81    operating_system: &'static str,
82    arch: &'static str,
83    env: BTreeMap<String, String>,
84
85    #[serde(skip_serializing_if = "Vec::is_empty")]
86    warnings: Vec<String>,
87}
88
89#[derive(Debug, Serialize)]
90#[allow(dead_code)]
91#[serde(rename_all = "camelCase")]
92struct LogFile {
93    filename: PathBuf,
94    exists: bool,
95    len: Option<u64>,
96}
97
98impl LogFile {
99    fn new(filename: PathBuf) -> Self {
100        Self {
101            filename: filename.clone(),
102            exists: filename.exists(),
103            len: if let Ok(meta) = filename.metadata() {
104                Some(meta.len())
105            } else {
106                None
107            },
108        }
109    }
110}
111
112fn output_of(bin: &str, args: &[&str]) -> anyhow::Result<(String, String)> {
113    let output = Command::new(bin).args(args).output()?;
114    if !output.status.success() {
115        return Err(anyhow!("command failed: {bin:?} {args:?}"));
116    }
117    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
118    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
119    Ok((stdout, stderr))
120}
121
122fn stdout_of(bin: &str, args: &[&str]) -> anyhow::Result<String> {
123    let (stdout, _) = output_of(bin, args)?;
124    Ok(stdout)
125}
126
127fn stderr_of(bin: &str, args: &[&str]) -> anyhow::Result<String> {
128    let (_, stderr) = output_of(bin, args)?;
129    Ok(stderr)
130}
131
132fn collect_warnings(profile: Option<&Profile>) -> Vec<String> {
133    match profile {
134        Some(profile) => crate::warning::config_warnings(&profile.config),
135        None => vec!["No Radicle profile found.".to_string()],
136    }
137}