Skip to main content

lb_rs/service/
debug.rs

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