radicle_cli/commands/
debug.rs

1#![allow(clippy::or_fun_call)]
2use std::collections::BTreeMap;
3use std::env;
4use std::ffi::OsString;
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;
14use crate::terminal::args::{Args, Help};
15
16pub const NAME: &str = "rad";
17pub const VERSION: &str = env!("RADICLE_VERSION");
18pub const DESCRIPTION: &str = "Radicle command line interface";
19pub const GIT_HEAD: &str = env!("GIT_HEAD");
20
21pub const HELP: Help = Help {
22    name: "debug",
23    description: "Write out information to help debug your Radicle node remotely",
24    version: env!("RADICLE_VERSION"),
25    usage: r#"
26Usage
27
28    rad debug
29
30    Run this if you are reporting a problem in Radicle. The output is
31    helpful for Radicle developers to debug your problem remotely. The
32    output is meant to not include any sensitive information, but
33    please check it, and then forward to the Radicle developers.
34
35"#,
36};
37
38#[derive(Debug)]
39pub struct Options {}
40
41impl Args for Options {
42    fn from_args(_args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
43        Ok((Options {}, vec![]))
44    }
45}
46
47pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
48    match ctx.profile() {
49        Ok(profile) => debug(Some(&profile)),
50        Err(e) => {
51            eprintln!("ERROR: Could not load Radicle profile: {e}");
52            debug(None)
53        }
54    }
55}
56
57// Collect information about the local Radicle installation and write
58// it out.
59fn debug(profile: Option<&Profile>) -> anyhow::Result<()> {
60    let env = BTreeMap::from_iter(env::vars().filter_map(|(k, v)| {
61        if k == "RAD_PASSPHRASE" {
62            Some((k, "<REDACTED>".into()))
63        } else if k.starts_with("RAD_") || k.starts_with("SSH_") || k == "PATH" || k == "SHELL" {
64            Some((k, v))
65        } else {
66            None
67        }
68    }));
69
70    let debug = DebugInfo {
71        rad_exe: if let Ok(filename) = std::env::current_exe() {
72            Some(filename)
73        } else {
74            None
75        },
76        rad_version: VERSION,
77        radicle_node_version: stdout_of("radicle-node", &["--version"])
78            .unwrap_or("<unknown>".into()),
79        git_remote_rad_version: stdout_of("git-remote-rad", &["--version"])
80            .unwrap_or("<unknown>".into()),
81        git_version: stdout_of("git", &["--version"]).unwrap_or("<unknown>".into()),
82        ssh_version: stderr_of("ssh", &["-V"]).unwrap_or("<unknown>".into()),
83        git_head: GIT_HEAD,
84        log: profile.map(|p| LogFile::new(p.node().join("node.log"))),
85        old_log: profile.map(|p| LogFile::new(p.node().join("node.log.old"))),
86        operating_system: std::env::consts::OS,
87        arch: std::env::consts::ARCH,
88        env,
89        warnings: collect_warnings(profile),
90    };
91
92    println!("{}", serde_json::to_string_pretty(&debug).unwrap());
93
94    Ok(())
95}
96
97#[derive(Debug, Serialize)]
98#[allow(dead_code)]
99#[serde(rename_all = "camelCase")]
100struct DebugInfo {
101    rad_exe: Option<PathBuf>,
102    rad_version: &'static str,
103    radicle_node_version: String,
104    git_remote_rad_version: String,
105    git_version: String,
106    ssh_version: String,
107    git_head: &'static str,
108    log: Option<LogFile>,
109    old_log: Option<LogFile>,
110    operating_system: &'static str,
111    arch: &'static str,
112    env: BTreeMap<String, String>,
113
114    #[serde(skip_serializing_if = "Vec::is_empty")]
115    warnings: Vec<String>,
116}
117
118#[derive(Debug, Serialize)]
119#[allow(dead_code)]
120#[serde(rename_all = "camelCase")]
121struct LogFile {
122    filename: PathBuf,
123    exists: bool,
124    len: Option<u64>,
125}
126
127impl LogFile {
128    fn new(filename: PathBuf) -> Self {
129        Self {
130            filename: filename.clone(),
131            exists: filename.exists(),
132            len: if let Ok(meta) = filename.metadata() {
133                Some(meta.len())
134            } else {
135                None
136            },
137        }
138    }
139}
140
141fn output_of(bin: &str, args: &[&str]) -> anyhow::Result<(String, String)> {
142    let output = Command::new(bin).args(args).output()?;
143    if !output.status.success() {
144        return Err(anyhow!("command failed: {bin:?} {args:?}"));
145    }
146    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
147    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
148    Ok((stdout, stderr))
149}
150
151fn stdout_of(bin: &str, args: &[&str]) -> anyhow::Result<String> {
152    let (stdout, _) = output_of(bin, args)?;
153    Ok(stdout)
154}
155
156fn stderr_of(bin: &str, args: &[&str]) -> anyhow::Result<String> {
157    let (_, stderr) = output_of(bin, args)?;
158    Ok(stderr)
159}
160
161fn collect_warnings(profile: Option<&Profile>) -> Vec<String> {
162    match profile {
163        Some(profile) => crate::warning::nodes_renamed(&profile.config),
164        None => vec!["No Radicle profile found.".to_string()],
165    }
166}