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::path::Path;
13
14#[cfg(not(target_family = "wasm"))]
15use std::env;
16
17#[cfg(not(target_family = "wasm"))]
18use chrono::{DateTime, 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 Lb {
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.as_ref().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.as_ref().copied() {
80 id
81 } else {
82 let new_id = LbID::generate();
83 db.id.replace(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_key(|panic| std::cmp::Reverse(panic.time));
106
107 Ok(panics)
108 }
109
110 #[cfg(not(target_family = "wasm"))]
112 pub async fn recent_panic(&self) -> LbResult<Option<DateTime<Local>>> {
113 let panics = self.collect_panics(false).await?;
114 Ok(panics
115 .first()
116 .and_then(|panic| Local.from_local_datetime(&panic.time).single()))
117 }
118
119 #[instrument(level = "debug", skip(self), err(Debug))]
120 #[cfg(not(target_family = "wasm"))]
121 pub async fn write_panic_to_file(&self, error_header: String, bt: String) -> LbResult<String> {
122 let file_name = generate_panic_filename(&self.config.writeable_path);
123 let content = generate_panic_content(&error_header, &bt);
124
125 let mut file = OpenOptions::new()
126 .create(true)
127 .append(true)
128 .open(&file_name)
129 .await?;
130
131 file.write_all(content.as_bytes()).await?;
132
133 Ok(file_name)
134 }
135
136 #[instrument(level = "debug", skip(self), err(Debug))]
137 #[cfg(not(target_family = "wasm"))]
138 pub async fn debug_info(&self, os_info: String, check_docs: bool) -> LbResult<DebugInfo> {
139 let account = self.get_account()?;
140
141 let arch = env::consts::ARCH;
142 let os = env::consts::OS;
143 let family = env::consts::FAMILY;
144
145 let (integrity, last_synced, panics, lb_id) = tokio::join!(
146 self.test_repo_integrity(check_docs),
147 self.human_last_synced(),
148 self.collect_panics(true),
149 self.lb_id()
150 );
151
152 let panics = panics?.into_iter().map(|panic| panic.content).collect();
153
154 let mut status = self.status().await;
155 status.space_used = None;
156 let status = format!("{status:?}");
157 let is_syncing = self.syncer.try_lock().is_ok();
158
159 Ok(DebugInfo {
160 time: self.now(),
161 name: account.username.clone(),
162 lb_version: get_code_version().into(),
163 lb_id: lb_id?,
164 rust_triple: format!("{arch}.{family}.{os}"),
165 server_url: account.api_url.clone(),
166 integrity: format!(
167 "{} {integrity:?}",
168 if check_docs { "" } else { "doc checks skipped" }
169 ),
170 lb_dir: self.config.writeable_path.clone(),
171 last_synced,
172 os_info,
173 status,
174 is_syncing,
175 panics,
176 })
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct PanicInfo {
182 pub time: NaiveDateTime,
183 pub file_path: PathBuf,
184 pub content: String,
185}
186
187#[cfg(not(target_family = "wasm"))]
188pub struct PanicFiles {
189 entries: tokio::fs::ReadDir,
190 base_path: PathBuf,
191}
192
193#[cfg(not(target_family = "wasm"))]
194pub struct PanicFile {
195 pub time: NaiveDateTime,
196 pub file_path: PathBuf,
197}
198
199#[cfg(not(target_family = "wasm"))]
200impl PanicFiles {
201 pub async fn next(&mut self) -> LbResult<Option<PanicFile>> {
202 const PREFIX: &str = "panic---";
203 const SUFFIX: &str = ".log";
204 const TIMESTAMP_FORMAT: &str = "%Y-%m-%d---%H-%M-%S";
205
206 while let Some(entry) = self.entries.next_entry().await? {
207 let file_name = entry.file_name().into_string().unwrap_or_default();
208 if !file_name.starts_with(PREFIX) || !file_name.ends_with(SUFFIX) {
209 continue;
210 }
211 let timestamp_str = &file_name[PREFIX.len()..file_name.len() - SUFFIX.len()];
212 let Ok(time) = NaiveDateTime::parse_from_str(timestamp_str, TIMESTAMP_FORMAT) else {
213 continue;
214 };
215 let file_path = self.base_path.join(file_name);
216 return Ok(Some(PanicFile { time, file_path }));
217 }
218 Ok(None)
219 }
220}
221
222#[cfg(not(target_family = "wasm"))]
223impl PanicFile {
224 pub async fn content(&self) -> LbResult<String> {
225 let contents = fs::read_to_string(&self.file_path).await?;
226 Ok(format!("time: {}: contents: {}", self.time, contents))
227 }
228}
229
230#[cfg(not(target_family = "wasm"))]
231pub(crate) async fn iter_panic_files(path: &str) -> LbResult<PanicFiles> {
232 let base_path = Path::new(path).to_path_buf();
233 let entries = fs::read_dir(&base_path).await?;
234 Ok(PanicFiles { entries, base_path })
235}
236
237#[cfg(not(target_family = "wasm"))]
239pub(crate) async fn latest_panic_time(path: &str) -> LbResult<Option<i64>> {
240 let mut iter = iter_panic_files(path).await?;
241 let mut max: Option<i64> = None;
242 while let Some(entry) = iter.next().await? {
243 let new_ts = entry.time.and_utc().timestamp_millis();
244 match &mut max {
245 Some(ts) => {
246 if new_ts > *ts {
247 *ts = new_ts;
248 }
249 }
250 None => max = Some(new_ts),
251 }
252 }
253 Ok(max)
254}
255
256pub fn generate_panic_filename(path: &str) -> String {
257 let timestamp = chrono::Local::now().format("%Y-%m-%d---%H-%M-%S");
258 format!("{path}/panic---{timestamp}.log")
259}
260
261pub fn generate_panic_content(panic_info: &str, bt: &str) -> String {
262 format!("INFO: {panic_info}\nBT: {bt}")
263}