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: 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}