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: std::env::current_exe().ok(),
72        rad_version: VERSION,
73        radicle_node_version: stdout_of("radicle-node", &["--version"])
74            .unwrap_or("radicle-node <unknown>".into()),
75        git_remote_rad_version: stdout_of("git-remote-rad", &["--version"])
76            .unwrap_or("git-remote-rad <unknown>".into()),
77        git_version: stdout_of("git", &["--version"]).unwrap_or("<unknown>".into()),
78        ssh_version: stderr_of("ssh", &["-V"]).unwrap_or("<unknown>".into()),
79        git_head: GIT_HEAD,
80        log: profile.map(|p| LogFile::new(p.node().join("node.log"))),
81        old_log: profile.map(|p| LogFile::new(p.node().join("node.log.old"))),
82        operating_system: std::env::consts::OS,
83        arch: std::env::consts::ARCH,
84        env,
85        warnings: collect_warnings(profile),
86    };
87
88    println!("{}", serde_json::to_string_pretty(&debug).unwrap());
89
90    Ok(())
91}
92
93#[derive(Debug, Serialize)]
94#[allow(dead_code)]
95#[serde(rename_all = "camelCase")]
96struct DebugInfo {
97    rad_exe: Option<PathBuf>,
98    rad_version: &'static str,
99    radicle_node_version: String,
100    git_remote_rad_version: String,
101    git_version: String,
102    ssh_version: String,
103    git_head: &'static str,
104    log: Option<LogFile>,
105    old_log: Option<LogFile>,
106    operating_system: &'static str,
107    arch: &'static str,
108    env: BTreeMap<String, String>,
109
110    #[serde(skip_serializing_if = "Vec::is_empty")]
111    warnings: Vec<String>,
112}
113
114#[derive(Debug, Serialize)]
115#[allow(dead_code)]
116#[serde(rename_all = "camelCase")]
117struct LogFile {
118    filename: PathBuf,
119    exists: bool,
120    len: Option<u64>,
121}
122
123impl LogFile {
124    fn new(filename: PathBuf) -> Self {
125        Self {
126            filename: filename.clone(),
127            exists: filename.exists(),
128            len: if let Ok(meta) = filename.metadata() {
129                Some(meta.len())
130            } else {
131                None
132            },
133        }
134    }
135}
136
137fn output_of(bin: &str, args: &[&str]) -> anyhow::Result<(String, String)> {
138    let output = Command::new(bin).args(args).output()?;
139    if !output.status.success() {
140        return Err(anyhow!("command failed: {bin:?} {args:?}"));
141    }
142    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
143    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
144    Ok((stdout, stderr))
145}
146
147fn stdout_of(bin: &str, args: &[&str]) -> anyhow::Result<String> {
148    let (stdout, _) = output_of(bin, args)?;
149    Ok(stdout)
150}
151
152fn stderr_of(bin: &str, args: &[&str]) -> anyhow::Result<String> {
153    let (_, stderr) = output_of(bin, args)?;
154    Ok(stderr)
155}
156
157fn collect_warnings(profile: Option<&Profile>) -> Vec<String> {
158    match profile {
159        Some(profile) => crate::warning::nodes_renamed(&profile.config),
160        None => vec!["No Radicle profile found.".to_string()],
161    }
162}