launcher/pages/troubleshoot/
tasks.rs1use crate::pages::troubleshoot::TroubleshootData;
2use crate::pages::troubleshoot::types::TroubleshootPageState;
3use crate::tasks::{TaskEvent, spawn_task};
4use sha2::{Digest, Sha256};
5use std::fs;
6use supreme_fs::{GameStorage, SnapshotEvent};
7
8#[cfg(feature = "local")]
9use std::io;
10#[cfg(feature = "local")]
11use std::sync::mpsc::Sender;
12#[cfg(feature = "local")]
13use supreme_diagnostics::DiagnosticsEvent;
14
15#[derive(Clone, Debug)]
16pub struct LocalProfileInfo {
17 pub filename: String,
18 pub name: Option<String>,
19 pub description: Option<String>,
20 pub version: Option<String>,
21 pub is_remote: bool,
22 pub is_valid: bool,
23}
24
25pub fn start_refresh_profiles(
26 ctx: eframe::egui::Context,
27 state: &mut TroubleshootPageState,
28 storage: &GameStorage,
29) {
30 init_task(state, "Refreshing profiles...".to_string());
31 let storage = storage.clone();
32
33 state.task_handle = Some(spawn_task(ctx, move |tx| {
34 let _ = tx.send(TaskEvent::SetStatus("Refreshing profiles...".to_string()));
35
36 let _ = tx.send(TaskEvent::PageData(TroubleshootData::RefreshStatus(true)));
38
39 let start_total = std::time::Instant::now();
40
41 let (local_tx, local_rx) = std::sync::mpsc::channel();
42 let (remote_tx, remote_rx) = std::sync::mpsc::channel();
43
44 let storage_local = storage.clone();
46 let local_tx_log = tx.clone();
47 std::thread::spawn(move || {
48 let _ = local_tx_log.send(TaskEvent::Log("Scanning local profiles...".to_string()));
49 let start_local = std::time::Instant::now();
50
51 let filenames = match storage_local.list_snapshots_profiles() {
52 Ok(f) => {
53 let _ = local_tx_log.send(TaskEvent::Log(format!(
54 "Found {} local profiles on disk.",
55 f.len()
56 )));
57 f
58 }
59 Err(e) => {
60 let _ = local_tx_log.send(TaskEvent::Log(format!(
61 "Warning: Failed to list local profiles: {e}"
62 )));
63 Vec::new()
64 }
65 };
66 let mut profiles = Vec::new();
67
68 for filename in filenames {
69 let filename_json = format!("{filename}.json");
70 let path = storage_local.profiles_path().join(&filename_json);
71 let mut name = None;
72 let mut desc = None;
73 let mut version = None;
74
75 let is_valid;
76 if let Ok(content) = fs::read(&path) {
77 let mut hasher = Sha256::new();
79 hasher.update(&content);
80 let actual_hash = hex::encode(hasher.finalize());
81 is_valid = actual_hash == filename;
82
83 if let Ok(manifest) =
84 serde_json::from_slice::<supreme_snapshot::SnapshotManifest>(&content)
85 {
86 name = manifest.name;
87 desc = manifest.description;
88 version = manifest.version;
89 }
90 } else {
91 is_valid = false;
92 }
93
94 profiles.push(LocalProfileInfo {
95 filename,
96 name,
97 description: desc,
98 version,
99 is_remote: false,
100 is_valid,
101 });
102 }
103 let _ = local_tx.send((profiles, start_local.elapsed().as_secs_f64() * 1000.0));
104 });
105
106 let storage_remote = storage.clone();
108 let remote_tx_log = tx.clone();
109 std::thread::spawn(move || {
110 let _ = remote_tx_log.send(TaskEvent::Log("Fetching online profiles...".to_string()));
111 let start_remote = std::time::Instant::now();
112
113 let online = match storage_remote.list_online_profiles() {
114 Ok(p) => {
115 let _ = remote_tx_log.send(TaskEvent::Log(format!(
116 "Found {} online profiles.",
117 p.len()
118 )));
119 p
120 }
121 Err(e) => {
122 let _ = remote_tx_log.send(TaskEvent::Log(format!(
123 "Warning: Failed to list online profiles: {e}"
124 )));
125 Vec::new()
126 }
127 };
128 let _ = remote_tx.send((online, start_remote.elapsed().as_secs_f64() * 1000.0));
129 });
130
131 let mut local_res: Option<Vec<LocalProfileInfo>> = None;
133 let mut remote_res: Option<Vec<supreme_fs::OnlineProfileInfo>> = None;
134 let mut local_ms = 0.0f64;
135 let mut remote_ms = 0.0f64;
136
137 let (local_profiles, online_profiles) = loop {
138 if local_res.is_none() {
139 match local_rx.try_recv() {
140 Ok((res, ms)) => {
141 local_res = Some(res);
142 local_ms = ms;
143 }
144 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
145 local_res = Some(Vec::new());
146 }
147 _ => {}
148 }
149 }
150
151 if remote_res.is_none() {
152 match remote_rx.try_recv() {
153 Ok((res, ms)) => {
154 remote_res = Some(res);
155 remote_ms = ms;
156 }
157 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
158 remote_res = Some(Vec::new());
159 }
160 _ => {}
161 }
162 }
163
164 if let (Some(local), Some(remote)) = (local_res.take(), remote_res.take()) {
165 break (local, remote);
166 }
167 std::thread::sleep(std::time::Duration::from_millis(10));
168 };
169
170 let mut final_profiles = Vec::new();
172 let mut processed_filenames = std::collections::HashSet::new();
173
174 let online_map: std::collections::HashMap<String, supreme_fs::OnlineProfileInfo> =
175 online_profiles
176 .into_iter()
177 .map(|p| (p.filename.clone(), p))
178 .collect();
179
180 for mut profile in local_profiles {
181 let filename_json = format!("{}.json", profile.filename);
182 if let Some(online_info) = online_map.get(&filename_json) {
183 profile.is_remote = true;
184 if profile.name.is_none() {
185 profile.name = online_info.name.clone();
186 }
187 if profile.description.is_none() {
188 profile.description = online_info.description.clone();
189 }
190 if profile.version.is_none() {
191 profile.version = online_info.version.clone();
192 }
193 }
194 processed_filenames.insert(filename_json);
195 final_profiles.push(profile);
196 }
197
198 for (filename_json, info) in &online_map {
200 if !processed_filenames.contains(filename_json) {
201 let stem = filename_json
202 .strip_suffix(".json")
203 .unwrap_or(filename_json)
204 .to_string();
205 final_profiles.push(LocalProfileInfo {
206 filename: stem,
207 name: info.name.clone(),
208 description: info.description.clone(),
209 version: info.version.clone(),
210 is_remote: true,
211 is_valid: true,
212 });
213 }
214 }
215
216 let count = final_profiles.len();
217 let _ = tx.send(TaskEvent::PageData(TroubleshootData::Profiles(
218 final_profiles,
219 )));
220 let _ = tx.send(TaskEvent::PageData(TroubleshootData::RefreshStatus(false)));
221 let _ = tx.send(TaskEvent::Log(format!("{count} total profiles loaded.")));
222 let _ = tx.send(TaskEvent::SetStatus(format!("{count} profiles loaded.")));
223
224 let total_ms = start_total.elapsed().as_secs_f64() * 1000.0;
225
226 #[derive(serde::Serialize)]
228 struct ListingPerf {
229 total_ms: f64,
230 local_ms: f64,
231 remote_ms: f64,
232 profile_count: usize,
233 timestamp: u64,
234 }
235
236 let perf = ListingPerf {
237 total_ms,
238 local_ms,
239 remote_ms,
240 profile_count: count,
241 timestamp: std::time::SystemTime::now()
242 .duration_since(std::time::UNIX_EPOCH)
243 .unwrap_or_default()
244 .as_secs(),
245 };
246 if let Err(e) = storage.write_report("listing_profiles_perf", &perf) {
247 let _ = tx.send(TaskEvent::Log(format!("Failed to write perf report: {e}")));
248 }
249
250 Ok(TroubleshootData::RefreshStatus(false)) }));
252}
253
254pub fn init_task(state: &mut TroubleshootPageState, status: String) {
255 state.is_running = true;
256 state.total_items = 0;
257 state.processed_items = 0;
258 state.status = status;
259 state.logs.clear();
260 state.performance_entries.clear();
261}
262
263#[cfg(feature = "local")]
264pub fn run_cleaner_task<F>(
265 ctx: eframe::egui::Context,
266 state: &mut TroubleshootPageState,
267 storage: &GameStorage,
268 name: &str,
269 f: F,
270) where
271 F: FnOnce(&GameStorage, Sender<supreme_cleaner::CleanerEvent>) -> io::Result<()>
272 + Send
273 + 'static,
274{
275 init_task(state, format!("{name}..."));
276 let name = name.to_string();
277 let storage = storage.clone();
278
279 state.task_handle = Some(spawn_task(ctx, move |tx| {
280 let _ = tx.send(TaskEvent::Log(format!("Starting: {name}")));
281 let (lib_tx, lib_rx) = std::sync::mpsc::channel();
282
283 let handle = std::thread::spawn(move || f(&storage, lib_tx));
284
285 for event in lib_rx {
286 let task_event: TaskEvent<()> = event.into();
287 let _ = tx.send(task_event.map(|_| TroubleshootData::RefreshStatus(false)));
288 }
289
290 let result = handle
291 .join()
292 .unwrap_or(Err(io::Error::other("Task panicked")));
293 match &result {
294 Err(e) => {
295 let _ = tx.send(TaskEvent::Log(format!("Error: {e}")));
296 }
297 Ok(_) => {
298 let _ = tx.send(TaskEvent::Log(format!("Completed: {name}")));
299 }
300 }
301 result
302 .map(|_| TroubleshootData::RefreshStatus(false))
303 .map_err(|e| e.to_string())
304 }));
305}
306
307pub fn start_create_snapshot(
308 ctx: eframe::egui::Context,
309 state: &mut TroubleshootPageState,
310 storage: &GameStorage,
311 request: SnapshotRequest,
312) {
313 init_task(state, format!("Creating snapshot '{}'...", request.name));
314 let SnapshotRequest {
315 name,
316 description,
317 version,
318 custom_directory,
319 compression_level,
320 } = request;
321 let storage = storage.clone();
322
323 state.task_handle = Some(spawn_task(ctx, move |tx| {
324 let (lib_tx, lib_rx) = std::sync::mpsc::channel::<SnapshotEvent>();
325
326 let handle = std::thread::spawn(move || {
328 supreme_snapshot::create_snapshot(
329 &storage,
330 &name,
331 description,
332 version,
333 custom_directory,
334 lib_tx,
335 compression_level,
336 )
337 });
338
339 for event in lib_rx {
340 let task_event: TaskEvent<()> = event.into();
341 let _ = tx.send(task_event.map(|()| TroubleshootData::RefreshStatus(false)));
342 }
343
344 handle
345 .join()
346 .unwrap_or(Err(std::io::Error::other("Thread panicked")))
347 .map(|_| TroubleshootData::RefreshStatus(false))
348 .map_err(|e| e.to_string())
349 }));
350}
351
352pub struct SnapshotRequest {
355 pub name: String,
356 pub description: Option<String>,
357 pub version: Option<String>,
358 pub custom_directory: Option<std::path::PathBuf>,
359 pub compression_level: u32,
360}
361
362pub fn start_restore_snapshot(
363 ctx: eframe::egui::Context,
364 state: &mut TroubleshootPageState,
365 storage: &GameStorage,
366 profile: String,
367) {
368 init_task(state, format!("Restoring profile '{profile}'..."));
369 let storage = storage.clone();
370
371 state.task_handle = Some(spawn_task(ctx, move |tx| {
372 let (lib_tx, lib_rx) = std::sync::mpsc::channel::<SnapshotEvent>();
373 let mut perf_entries: Vec<(String, String, u128)> = Vec::new();
374
375 let profile_for_thread = profile.clone();
377 let storage_for_thread = storage.clone();
378 let handle = std::thread::spawn(move || {
379 supreme_snapshot::restore_snapshot(&storage_for_thread, &profile_for_thread, lib_tx)
380 });
381
382 for event in lib_rx {
383 if let SnapshotEvent::Perf(ref op, ref target, ms) = event {
384 perf_entries.push((op.clone(), target.clone(), ms));
385 }
386 let task_event: TaskEvent<()> = event.into();
387 let _ = tx.send(task_event.map(|()| TroubleshootData::RefreshStatus(false)));
388 }
389
390 let result = handle
391 .join()
392 .unwrap_or(Err(std::io::Error::other("Thread panicked")));
393
394 #[derive(serde::Serialize)]
395 struct PerfEntry {
396 op: String,
397 target: String,
398 ms: u128,
399 }
400 #[derive(serde::Serialize)]
401 struct RestorePerf {
402 profile: String,
403 entries: Vec<PerfEntry>,
404 timestamp: u64,
405 }
406 let report = RestorePerf {
407 profile: profile.clone(),
408 entries: perf_entries
409 .iter()
410 .map(|(op, target, ms)| PerfEntry {
411 op: op.clone(),
412 target: target.clone(),
413 ms: *ms,
414 })
415 .collect(),
416 timestamp: std::time::SystemTime::now()
417 .duration_since(std::time::UNIX_EPOCH)
418 .unwrap_or_default()
419 .as_secs(),
420 };
421 if let Err(e) = storage.write_report("restore_snapshot_perf", &report) {
422 let _ = tx.send(TaskEvent::Log(format!("Failed to write perf report: {e}")));
423 }
424
425 if let Ok(ref manifest) = result {
428 let sub_tx = tx.clone();
429 let (lib_tx, lib_rx) = std::sync::mpsc::channel::<SnapshotEvent>();
430 let bridge_handle = std::thread::spawn(move || {
431 while let Ok(event) = lib_rx.recv() {
432 let task_event: TaskEvent<()> = event.into();
433 let _ =
434 sub_tx.send(task_event.map(|()| TroubleshootData::RefreshStatus(false)));
435 }
436 });
437
438 if let Err(e) =
439 supreme_snapshot::reconcile_music_after_restore(&storage, manifest, &lib_tx)
440 {
441 let _ = tx.send(TaskEvent::Log(format!(
442 "Failed to reconcile music after restore: {e}"
443 )));
444 }
445 drop(lib_tx);
446 if bridge_handle.join().is_err() {
447 let _ = tx.send(TaskEvent::Log(
448 "Music reconciliation logging thread panicked".to_string(),
449 ));
450 }
451 }
452
453 result
454 .map(|_manifest| TroubleshootData::RefreshStatus(false))
455 .map_err(|e| e.to_string())
456 }));
457}
458
459pub fn start_upload_snapshot(
460 ctx: eframe::egui::Context,
461 state: &mut TroubleshootPageState,
462 storage: &GameStorage,
463 filename: String,
464) {
465 init_task(state, format!("Uploading snapshot '{filename}'..."));
466 let storage = storage.clone();
467
468 state.task_handle = Some(spawn_task(ctx, move |tx| {
469 let (lib_tx, lib_rx) = std::sync::mpsc::channel::<SnapshotEvent>();
470
471 let path = storage.profiles_path().join(format!("{filename}.json"));
472
473 if let Ok(metadata) = fs::metadata(&path) {
474 let file_size = metadata.len();
475 let _ = tx.send(TaskEvent::Log(format!("Snapshot size: {file_size} bytes")));
476
477 if let Ok(max_size) = storage.get_max_upload_size()
478 && file_size > max_size as u64
479 {
480 let _ = tx.send(TaskEvent::Log(format!(
481 "File size exceeds server limit ({max_size} bytes), using chunked upload"
482 )));
483 }
484 }
485
486 let handle = std::thread::spawn(move || {
488 supreme_snapshot::upload_snapshot(&storage, &path, &filename, &lib_tx)
489 });
490
491 for event in lib_rx {
492 let task_event: TaskEvent<()> = event.into();
493 let _ = tx.send(task_event.map(|()| TroubleshootData::RefreshStatus(false)));
494 }
495
496 handle
497 .join()
498 .unwrap_or(Err(std::io::Error::other("Thread panicked")))
499 .map(|()| TroubleshootData::RefreshStatus(false))
500 .map_err(|e| e.to_string())
501 }));
502}
503
504#[cfg(feature = "local")]
505pub fn start_path_scan(
506 ctx: eframe::egui::Context,
507 state: &mut TroubleshootPageState,
508 storage: &GameStorage,
509) {
510 init_task(state, "Scanning for broken paths...".to_string());
511 let storage = storage.clone();
512
513 state.task_handle = Some(spawn_task(ctx, move |tx| {
514 let (lib_tx, lib_rx) = std::sync::mpsc::channel::<DiagnosticsEvent>();
515
516 let handle = std::thread::spawn(move || supreme_diagnostics::scan_paths(&storage, lib_tx));
518
519 for event in lib_rx {
520 if !matches!(event, DiagnosticsEvent::Finished(_)) {
523 let task_event: TaskEvent<()> = event.into();
524 let _ = tx.send(task_event.map(|_| TroubleshootData::RefreshStatus(false)));
525 }
526 }
527
528 match handle
529 .join()
530 .unwrap_or(Err(std::io::Error::other("Thread panicked")))
531 {
532 Ok(diagnostics) => {
533 let _ = tx.send(TaskEvent::PageData(TroubleshootData::Diagnostics(
534 diagnostics,
535 )));
536 Ok(TroubleshootData::RefreshStatus(false))
537 }
538 Err(e) => Err(e.to_string()),
539 }
540 }));
541}