Skip to main content

lb_rs/service/
debug.rs

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