Skip to main content

lb_rs/service/
debug.rs

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