lb_rs/service/
debug.rs

1use crate::model::clock;
2use crate::model::errors::LbResult;
3use crate::Lb;
4use crate::{get_code_version, service::logging::LOG_FILE};
5use basic_human_duration::ChronoHumanDuration;
6use chrono::NaiveDateTime;
7use serde::Serialize;
8use std::env;
9use std::io::SeekFrom;
10use std::path::{Path, PathBuf};
11use time::Duration;
12use tokio::fs::{self, File};
13use tokio::io::{AsyncReadExt, AsyncSeekExt};
14
15#[derive(Serialize)]
16pub struct DebugInfo {
17    pub time: String,
18    pub name: String,
19    pub last_synced: String,
20    pub lb_version: String,
21    pub rust_triple: String,
22    pub os_info: String,
23    pub lb_dir: String,
24    pub server_url: String,
25    pub integrity: String,
26    pub log_tail: String,
27    pub last_panic: String,
28}
29
30impl Lb {
31    async fn tail_log(&self) -> LbResult<String> {
32        let mut path = PathBuf::from(&self.config.writeable_path);
33        if path.exists() {
34            path.push(LOG_FILE);
35            let mut file = File::open(path).await?;
36            let size = file.metadata().await?.len();
37            let read_amount = 5 * 1024;
38            let pos = if read_amount > size { 0 } else { size - read_amount };
39
40            let mut buffer = Vec::with_capacity(read_amount as usize);
41            file.seek(SeekFrom::Start(pos)).await?;
42            file.read_to_end(&mut buffer).await?;
43            if self.config.colored_logs {
44                // strip colors
45                buffer = strip_ansi_escapes::strip(buffer);
46            }
47            Ok(String::from_utf8_lossy(&buffer).to_string())
48        } else {
49            Ok("NO LOGS FOUND".to_string())
50        }
51    }
52
53    async fn human_last_synced(&self) -> String {
54        let tx = self.ro_tx().await;
55        let db = tx.db();
56
57        let last_synced = *db.last_synced.get().unwrap_or(&0);
58
59        if last_synced != 0 {
60            Duration::milliseconds(clock::get_time().0 - last_synced)
61                .format_human()
62                .to_string()
63        } else {
64            "never".to_string()
65        }
66    }
67
68    fn now(&self) -> String {
69        let now = chrono::Local::now();
70        now.format("%Y-%m-%d %H:%M:%S %Z").to_string()
71    }
72
73    async fn find_most_recent_panic_log(&self) -> LbResult<String> {
74        let dir_path = &self.config.writeable_path;
75        let path = Path::new(dir_path);
76
77        let prefix = "panic---";
78        let suffix = ".log";
79        let timestamp_format = "%Y-%m-%d---%H-%M-%S";
80
81        let mut most_recent_file: Option<String> = None;
82        let mut most_recent_time: Option<NaiveDateTime> = None;
83
84        let mut entries = fs::read_dir(path).await?;
85        while let Some(entry) = entries.next_entry().await? {
86            let file_name = entry.file_name().into_string().unwrap_or_default();
87
88            // Check if the filename matches the expected format
89            if file_name.starts_with(prefix) && file_name.ends_with(suffix) {
90                // Extract the timestamp portion from the filename
91                let timestamp_str = &file_name[prefix.len()..file_name.len() - suffix.len()];
92
93                // Parse the timestamp
94                if let Ok(timestamp) =
95                    NaiveDateTime::parse_from_str(timestamp_str, timestamp_format)
96                {
97                    // Compare to find the most recent timestamp
98                    match most_recent_time {
99                        Some(ref current_most_recent) => {
100                            if timestamp > *current_most_recent {
101                                most_recent_time = Some(timestamp);
102                                most_recent_file = Some(file_name.clone());
103                            }
104                        }
105                        None => {
106                            most_recent_time = Some(timestamp);
107                            most_recent_file = Some(file_name.clone());
108                        }
109                    }
110                }
111            }
112        }
113
114        // If we found the most recent file, read its contents
115        if let Some(file_name) = most_recent_file {
116            let file_path = path.join(file_name);
117            let contents = fs::read_to_string(file_path).await?;
118            Ok(contents)
119        } else {
120            Ok(String::default())
121        }
122    }
123
124    #[instrument(level = "debug", skip(self), err(Debug))]
125    pub async fn debug_info(&self, os_info: String) -> LbResult<String> {
126        let account = self.get_account()?;
127
128        let arch = env::consts::ARCH;
129        let os = env::consts::OS;
130        let family = env::consts::FAMILY;
131
132        let (integrity, log_tail, last_synced, last_panic) = tokio::join!(
133            self.test_repo_integrity(),
134            self.tail_log(),
135            self.human_last_synced(),
136            self.find_most_recent_panic_log()
137        );
138
139        Ok(serde_json::to_string_pretty(&DebugInfo {
140            time: self.now(),
141            name: account.username.clone(),
142            lb_version: get_code_version().into(),
143            rust_triple: format!("{arch}.{family}.{os}"),
144            server_url: account.api_url.clone(),
145            integrity: format!("{:?}", integrity),
146            log_tail: log_tail?,
147            lb_dir: self.config.writeable_path.clone(),
148            last_synced,
149            os_info,
150            last_panic: last_panic?,
151        })?)
152    }
153}