Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 18 additions & 20 deletions src-tauri/src/commands/patcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use crate::legacy_patcher::runner::{
run_legacy_patcher_loop, LegacyPatcherLoopError, DEFAULT_HOOK_TIMEOUT_MS,
};
use crate::mods::ModLibraryState;
use crate::overlay;
use crate::patcher::{PatcherPhase, PatcherState, StoredPatcherConfig};
use crate::state::SettingsState;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -218,26 +217,25 @@ pub(crate) fn start_patcher_inner(

let handle = thread::spawn(move || {
// Phase 1: Build overlay (the slow part)
let overlay_root =
match overlay::ensure_overlay(&library_clone, &settings_snapshot, &workshop_paths) {
Ok(root) => root,
Err(e) => {
tracing::error!(error = ?e, "Overlay build failed");
let error_response: AppErrorResponse = e.into();
let _ = library_clone
.app_handle()
.emit("patcher-error", &error_response);
if let Ok(mut s) = state_arc.lock() {
s.phase = PatcherPhase::Idle;
}
// TRAY: Reset to default on error
let _ = crate::tray::set_tray_state(
app_handle_thread.clone(),
crate::tray::AppTrayState::Default,
);
return;
let overlay_root = match library_clone.ensure_overlay(&settings_snapshot, &workshop_paths) {
Ok(root) => root,
Err(e) => {
tracing::error!(error = ?e, "Overlay build failed");
let error_response: AppErrorResponse = e.into();
let _ = library_clone
.app_handle()
.emit("patcher-error", &error_response);
if let Ok(mut s) = state_arc.lock() {
s.phase = PatcherPhase::Idle;
}
};
// TRAY: Reset to default on error
let _ = crate::tray::set_tray_state(
app_handle_thread.clone(),
crate::tray::AppTrayState::Default,
);
return;
}
};

// Check stop flag between build and patcher loop
if stop_flag.load(Ordering::SeqCst) {
Expand Down
43 changes: 40 additions & 3 deletions src-tauri/src/mods/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,17 +437,54 @@ pub(super) fn load_library_index(storage_dir: &Path) -> AppResult<LibraryIndex>
return Ok(LibraryIndex::default());
}

LibraryIndex::load_and_migrate(storage_dir)
match LibraryIndex::load_and_migrate(storage_dir) {
Ok(index) => Ok(index),
// Version conflicts and IO errors must surface — the former is a user-visible
// compatibility issue; the latter may indicate permissions or disk problems
// that the user needs to address (and not silently overwrite).
Err(e @ AppError::SchemaVersionTooNew { .. }) | Err(e @ AppError::Io(_)) => Err(e),
Err(e) => {
// JSON parse failure or structural mismatch means the file content is
// corrupt (e.g. truncated mid-write). Back it up for diagnostics and
// reset to defaults so the app can recover.
tracing::warn!(
"Library index content is corrupt ({}); resetting to defaults",
e
);
let corrupt_path = path.with_extension("json.corrupt");
if let Err(rename_err) = fs::rename(&path, &corrupt_path) {
tracing::warn!(
"Failed to rename corrupt library index to {}: {}",
corrupt_path.display(),
rename_err
);
}
Ok(LibraryIndex::default())
}
}
}

pub(super) fn save_library_index(storage_dir: &Path, index: &LibraryIndex) -> AppResult<()> {
fs::create_dir_all(storage_dir)?;
let path = library_index_path(storage_dir);
// Ensure the version field is always current when writing
let mut to_save = index.clone();
to_save.version = schema_migration::CURRENT_VERSION;
let contents = serde_json::to_string_pretty(&to_save)?;
fs::write(path, contents)?;
atomic_write_json(&path, &contents)?;
Ok(())
}

/// Write `contents` to `path` atomically via a sibling `.json.tmp` file.
///
/// A plain `fs::write` can leave `path` empty if the process is killed
/// mid-write; the rename is atomic on all supported platforms so the
/// destination is either the old version or the new version, never partial.
pub(super) fn atomic_write_json(path: &Path, contents: &str) -> AppResult<()> {
let tmp = path.with_extension("json.tmp");

fs::write(&tmp, contents)?;
fs::rename(&tmp, path)?;

Ok(())
}

Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/mods/schema_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use serde_json::Value;
use std::fs;
use std::path::Path;

use super::{library_index_path, LibraryIndex, ROOT_FOLDER_ID};
use super::{atomic_write_json, library_index_path, LibraryIndex, ROOT_FOLDER_ID};

/// Current schema version for the library index.
/// Increment this when making breaking changes to the schema and add a
Expand Down Expand Up @@ -50,7 +50,7 @@ impl LibraryIndex {

if migrated {
let contents = serde_json::to_string_pretty(&index)?;
fs::write(&path, contents)?;
atomic_write_json(&path, &contents)?;
}

Ok(index)
Expand Down
Loading
Loading