#![allow(clippy::or_fun_call)]
use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command;
use anyhow::anyhow;
use serde::Serialize;
use radicle::Profile;
use crate::terminal as term;
use crate::terminal::args::{Args, Help};
pub const NAME: &str = "rad";
pub const VERSION: &str = env!("RADICLE_VERSION");
pub const DESCRIPTION: &str = "Radicle command line interface";
pub const GIT_HEAD: &str = env!("GIT_HEAD");
pub const HELP: Help = Help {
name: "debug",
description: "Write out information to help debug your Radicle node remotely",
version: env!("RADICLE_VERSION"),
usage: r#"
Usage
rad debug
Run this if you are reporting a problem in Radicle. The output is
helpful for Radicle developers to debug your problem remotely. The
output is meant to not include any sensitive information, but
please check it, and then forward to the Radicle developers.
"#,
};
#[derive(Debug)]
pub struct Options {}
impl Args for Options {
fn from_args(_args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
Ok((Options {}, vec![]))
}
}
pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
match ctx.profile() {
Ok(profile) => debug(Some(&profile)),
Err(e) => {
eprintln!("ERROR: Could not load Radicle profile: {e}");
debug(None)
}
}
}
fn debug(profile: Option<&Profile>) -> anyhow::Result<()> {
let env = HashMap::from_iter(env::vars().filter_map(|(k, v)| {
if k == "RAD_PASSPHRASE" {
Some((k, "<REDACTED>".into()))
} else if k.starts_with("RAD_") || k.starts_with("SSH_") || k == "PATH" || k == "SHELL" {
Some((k, v))
} else {
None
}
}));
let debug = DebugInfo {
rad_exe: if let Ok(filename) = std::env::current_exe() {
Some(filename)
} else {
None
},
rad_version: VERSION,
radicle_node_version: stdout_of("radicle-node", &["--version"])
.unwrap_or("<unknown>".into()),
git_remote_rad_version: stdout_of("git-remote-rad", &["--version"])
.unwrap_or("<unknown>".into()),
git_version: stdout_of("git", &["--version"]).unwrap_or("<unknown>".into()),
ssh_version: stderr_of("ssh", &["-V"]).unwrap_or("<unknown>".into()),
git_head: GIT_HEAD,
log: profile.map(|p| LogFile::new(p.node().join("node.log"))),
old_log: profile.map(|p| LogFile::new(p.node().join("node.log.old"))),
operating_system: std::env::consts::OS,
arch: std::env::consts::ARCH,
env,
};
println!("{}", serde_json::to_string_pretty(&debug).unwrap());
Ok(())
}
#[derive(Debug, Serialize)]
#[allow(dead_code)]
#[serde(rename_all = "camelCase")]
struct DebugInfo {
rad_exe: Option<PathBuf>,
rad_version: &'static str,
radicle_node_version: String,
git_remote_rad_version: String,
git_version: String,
ssh_version: String,
git_head: &'static str,
log: Option<LogFile>,
old_log: Option<LogFile>,
operating_system: &'static str,
arch: &'static str,
env: HashMap<String, String>,
}
#[derive(Debug, Serialize)]
#[allow(dead_code)]
#[serde(rename_all = "camelCase")]
struct LogFile {
filename: PathBuf,
exists: bool,
len: Option<u64>,
}
impl LogFile {
fn new(filename: PathBuf) -> Self {
Self {
filename: filename.clone(),
exists: filename.exists(),
len: if let Ok(meta) = filename.metadata() {
Some(meta.len())
} else {
None
},
}
}
}
fn output_of(bin: &str, args: &[&str]) -> anyhow::Result<(String, String)> {
let output = Command::new(bin).args(args).output()?;
if !output.status.success() {
return Err(anyhow!("command failed: {bin:?} {args:?}"));
}
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Ok((stdout, stderr))
}
fn stdout_of(bin: &str, args: &[&str]) -> anyhow::Result<String> {
let (stdout, _) = output_of(bin, args)?;
Ok(stdout)
}
fn stderr_of(bin: &str, args: &[&str]) -> anyhow::Result<String> {
let (_, stderr) = output_of(bin, args)?;
Ok(stderr)
}