1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#![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)
        }
    }
}

// Collect information about the local Radicle installation and write
// it out.
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)
}