radicle_cli/commands/
debug.rs1#![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
57fn 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}