Skip to main content

lb_rs/service/
debug.rs

1use crate::Lb;
2use crate::model::clock;
3use crate::model::errors::LbResult;
4use crate::service::lb_id::LbID;
5use basic_human_duration::ChronoHumanDuration;
6use chrono::NaiveDateTime;
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9use time::Duration;
10
11#[cfg(not(target_family = "wasm"))]
12use std::sync::atomic::Ordering;
13
14#[cfg(not(target_family = "wasm"))]
15use std::path::Path;
16
17#[cfg(not(target_family = "wasm"))]
18use std::env;
19
20#[cfg(not(target_family = "wasm"))]
21use chrono::{Local, TimeZone};
22
23#[cfg(not(target_family = "wasm"))]
24use crate::get_code_version;
25
26#[cfg(not(target_family = "wasm"))]
27use tokio::fs::{self, OpenOptions};
28
29#[cfg(not(target_family = "wasm"))]
30use tokio::io::AsyncWriteExt;
31
32#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
33pub struct DebugInfo {
34    pub lb_id: LbID,
35    pub time: String,
36    pub name: String,
37    pub last_synced: String,
38    pub lb_version: String,
39    pub rust_triple: String,
40    pub os_info: String,
41    pub lb_dir: String,
42    pub server_url: String,
43    pub integrity: String,
44    pub is_syncing: bool,
45    pub status: String,
46    pub panics: Vec<String>,
47}
48
49pub trait DebugInfoDisplay {
50    fn to_string(&self) -> String;
51}
52
53impl DebugInfoDisplay for LbResult<DebugInfo> {
54    fn to_string(&self) -> String {
55        match self {
56            Ok(debug_info) => serde_json::to_string_pretty(debug_info).unwrap_or_default(),
57            Err(err) => format!("Error retrieving debug info: {:?}", err),
58        }
59    }
60}
61
62impl Lb {
63    async fn human_last_synced(&self) -> String {
64        let tx = self.ro_tx().await;
65        let db = tx.db();
66
67        let last_synced = *db.last_synced.get().unwrap_or(&0);
68
69        if last_synced != 0 {
70            Duration::milliseconds(clock::get_time().0 - last_synced)
71                .format_human()
72                .to_string()
73        } else {
74            "never".to_string()
75        }
76    }
77
78    async fn lb_id(&self) -> LbResult<LbID> {
79        let mut tx = self.begin_tx().await;
80        let db = tx.db();
81
82        let lb_id = if let Some(id) = db.id.get().copied() {
83            id
84        } else {
85            let new_id = LbID::generate();
86            db.id.insert(new_id)?;
87            new_id
88        };
89
90        tx.end();
91
92        Ok(lb_id)
93    }
94
95    fn now(&self) -> String {
96        let now = chrono::Local::now();
97        now.format("%Y-%m-%d %H:%M:%S %Z").to_string()
98    }
99
100    #[cfg(not(target_family = "wasm"))]
101    async fn collect_panics(&self, populate_content: bool) -> LbResult<Vec<PanicInfo>> {
102        let mut panics = vec![];
103
104        let dir_path = &self.config.writeable_path;
105        let path = Path::new(dir_path);
106
107        let prefix = "panic---";
108        let suffix = ".log";
109        let timestamp_format = "%Y-%m-%d---%H-%M-%S";
110
111        let mut entries = fs::read_dir(path).await?;
112        while let Some(entry) = entries.next_entry().await? {
113            let file_name = entry.file_name().into_string().unwrap_or_default();
114
115            // Check if the filename matches the expected format
116            if file_name.starts_with(prefix) && file_name.ends_with(suffix) {
117                // Extract the timestamp portion from the filename
118                let timestamp_str = &file_name[prefix.len()..file_name.len() - suffix.len()];
119
120                // Parse the timestamp
121                if let Ok(time) = NaiveDateTime::parse_from_str(timestamp_str, timestamp_format) {
122                    let file_path = path.join(file_name);
123                    let content = if populate_content {
124                        let contents = fs::read_to_string(&file_path).await?;
125                        let contents = format!("time: {time}: contents: {contents}");
126                        contents
127                    } else {
128                        Default::default()
129                    };
130
131                    panics.push(PanicInfo { time, file_path, content });
132                }
133            }
134        }
135        panics.sort_by(|a, b| b.time.cmp(&a.time));
136
137        Ok(panics)
138    }
139
140    /// returns true if we have crashed within the last 5 seconds
141    #[cfg(not(target_family = "wasm"))]
142    pub async fn recent_panic(&self) -> LbResult<bool> {
143        let panics = self.collect_panics(false).await?;
144        for panic in panics {
145            let timestamp_local_time = Local
146                .from_local_datetime(&panic.time)
147                .single()
148                .unwrap_or_default();
149
150            let seconds_ago = (Local::now() - timestamp_local_time).abs().num_seconds();
151
152            if seconds_ago <= 5 {
153                return Ok(true);
154            }
155        }
156
157        Ok(false)
158    }
159
160    #[instrument(level = "debug", skip(self), err(Debug))]
161    #[cfg(not(target_family = "wasm"))]
162    pub async fn write_panic_to_file(&self, error_header: String, bt: String) -> LbResult<String> {
163        let file_name = generate_panic_filename(&self.config.writeable_path);
164        let content = generate_panic_content(&error_header, &bt);
165
166        let mut file = OpenOptions::new()
167            .create(true)
168            .append(true)
169            .open(&file_name)
170            .await?;
171
172        file.write_all(content.as_bytes()).await?;
173
174        Ok(file_name)
175    }
176
177    #[instrument(level = "debug", skip(self), err(Debug))]
178    #[cfg(not(target_family = "wasm"))]
179    pub async fn debug_info(&self, os_info: String, check_docs: bool) -> LbResult<DebugInfo> {
180        let account = self.get_account()?;
181
182        let arch = env::consts::ARCH;
183        let os = env::consts::OS;
184        let family = env::consts::FAMILY;
185
186        let (integrity, last_synced, panics, lb_id) = tokio::join!(
187            self.test_repo_integrity(check_docs),
188            self.human_last_synced(),
189            self.collect_panics(true),
190            self.lb_id()
191        );
192
193        let panics = panics?.into_iter().map(|panic| panic.content).collect();
194
195        let mut status = self.status().await;
196        status.space_used = None;
197        let status = format!("{status:?}");
198        let is_syncing = self.syncing.load(Ordering::Relaxed);
199
200        Ok(DebugInfo {
201            time: self.now(),
202            name: account.username.clone(),
203            lb_version: get_code_version().into(),
204            lb_id: lb_id?,
205            rust_triple: format!("{arch}.{family}.{os}"),
206            server_url: account.api_url.clone(),
207            integrity: format!(
208                "{} {integrity:?}",
209                if check_docs { "" } else { "doc checks skipped" }
210            ),
211            lb_dir: self.config.writeable_path.clone(),
212            last_synced,
213            os_info,
214            status,
215            is_syncing,
216            panics,
217        })
218    }
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct PanicInfo {
223    pub time: NaiveDateTime,
224    pub file_path: PathBuf,
225    pub content: String,
226}
227
228pub fn generate_panic_filename(path: &str) -> String {
229    let timestamp = chrono::Local::now().format("%Y-%m-%d---%H-%M-%S");
230    format!("{path}/panic---{timestamp}.log")
231}
232
233pub fn generate_panic_content(panic_info: &str, bt: &str) -> String {
234    format!("INFO: {panic_info}\nBT: {bt}")
235}