Skip to main content

launcher/pages/troubleshoot/
tasks.rs

1use 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        // Send profile-refresh-active flag via the unified data channel
37        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        // Local scan, intentionally parallel sub-threads (short-lived)
45        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                    // Check validity: sha256 of content must match filename
78                    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        // Remote scan, intentionally parallel sub-thread
107        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        // Coordinator: wait for both results
132        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        // Merge results
171        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        // Add remote-only profiles
199        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        // Write performance report
227        #[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)) // Finisher data
251    }));
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        // Library must run on a std thread, it may use rayon internally.
327        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
352/// The user-supplied create-snapshot request, exactly as the form
353/// filled it in.
354pub 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        // Library must run on a std thread, it may use rayon internally.
376        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        // --- Post-Restoration Music Handling ---
426        // Pass the manifest returned by restore_snapshot directly so it is not re-loaded from disk.
427        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        // Library must run on a std thread, it may use rayon internally.
487        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        // Library must run on a std thread, it uses rayon::par_iter internally.
517        let handle = std::thread::spawn(move || supreme_diagnostics::scan_paths(&storage, lib_tx));
518
519        for event in lib_rx {
520            // DiagnosticsEvent::Finished carries Result<(), String> (not PathDiagnostics).
521            // The actual PathDiagnostics is the function's return value; skip this variant here.
522            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}