radicle_cli/commands/
debug.rs

1#![allow(clippy::or_fun_call)]
2use std::collections::HashMap;
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 = HashMap::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    };
90
91    println!("{}", serde_json::to_string_pretty(&debug).unwrap());
92
93    Ok(())
94}
95
96#[derive(Debug, Serialize)]
97#[allow(dead_code)]
98#[serde(rename_all = "camelCase")]
99struct DebugInfo {
100    rad_exe: Option<PathBuf>,
101    rad_version: &'static str,
102    radicle_node_version: String,
103    git_remote_rad_version: String,
104    git_version: String,
105    ssh_version: String,
106    git_head: &'static str,
107    log: Option<LogFile>,
108    old_log: Option<LogFile>,
109    operating_system: &'static str,
110    arch: &'static str,
111    env: HashMap<String, 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}