feat: store launcher state outside game dirs
Move launcher-owned metadata from game roots into the configured peer state area. Peer identity, the local library index, install intent logs, and setup markers now live under app/CLI state instead of being written beside games. The Tauri shell passes its app data directory into the peer, and the peer CLI runs the same path through its explicit --state-dir. Add a dedicated pre-start migration phase for legacy files. It migrates the old global library index, per-game install intents, and the old first-start marker into app state, then deletes legacy files only after the replacement write succeeds. Normal scan, install, recovery, and transfer paths no longer read legacy state files. Rename the old first-start meaning to setup_done and only set it after launching game_setup.cmd. Start/setup scripts keep the shared argument shape, while server_start.cmd now uses cmd /k and a visible window so server logs stay open for inspection. While validating the Docker scenario matrix, make download terminal events come from the handler after local state refresh and operation cleanup. This makes download-finished/download-failed safe points for immediate follow-up CLI commands. Also update the multi-peer chunking scenario to use a sparse archive large enough to actually span multiple production chunks. Test Plan: - just fmt - just test - just frontend-test - just build - just clippy - git diff --check - python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py Refs: local app-state migration discussion
This commit is contained in:
@@ -71,7 +71,7 @@ pub async fn local_download_available(
|
||||
// Local library index and scanning
|
||||
// =============================================================================
|
||||
|
||||
const LIBRARY_INDEX_DIR: &str = ".lanspread";
|
||||
const LEGACY_LIBRARY_INDEX_DIR: &str = ".lanspread";
|
||||
const LIBRARY_INDEX_FILE: &str = "library_index.json";
|
||||
const INTENT_LOG_FILE: &str = ".lanspread.json";
|
||||
const VERSION_TMP_FILE: &str = ".version.ini.tmp";
|
||||
@@ -114,8 +114,14 @@ pub struct LocalLibraryScan {
|
||||
pub revision: u64,
|
||||
}
|
||||
|
||||
fn library_index_path(game_dir: &Path) -> PathBuf {
|
||||
game_dir.join(LIBRARY_INDEX_DIR).join(LIBRARY_INDEX_FILE)
|
||||
pub(crate) fn legacy_library_index_path(game_dir: &Path) -> PathBuf {
|
||||
game_dir
|
||||
.join(LEGACY_LIBRARY_INDEX_DIR)
|
||||
.join(LIBRARY_INDEX_FILE)
|
||||
}
|
||||
|
||||
fn library_index_path(state_dir: &Path) -> PathBuf {
|
||||
crate::state_paths::local_library_index_path(state_dir)
|
||||
}
|
||||
|
||||
fn library_index_tmp_path(path: &Path) -> PathBuf {
|
||||
@@ -278,7 +284,7 @@ async fn fingerprint_game_dir(game_path: &Path) -> eyre::Result<GameFingerprint>
|
||||
}
|
||||
|
||||
pub fn is_ignored_game_root_name(name: &str) -> bool {
|
||||
name == LIBRARY_INDEX_DIR
|
||||
name == LEGACY_LIBRARY_INDEX_DIR
|
||||
}
|
||||
|
||||
fn is_reserved_transient_name(name: &str) -> bool {
|
||||
@@ -286,7 +292,7 @@ fn is_reserved_transient_name(name: &str) -> bool {
|
||||
|| name == VERSION_TMP_FILE
|
||||
|| name == VERSION_DISCARDED_FILE
|
||||
|| name == INTENT_LOG_FILE
|
||||
|| name == LIBRARY_INDEX_DIR
|
||||
|| name == LEGACY_LIBRARY_INDEX_DIR
|
||||
}
|
||||
|
||||
fn should_skip_root_entry(entry: &walkdir::DirEntry) -> bool {
|
||||
@@ -550,9 +556,11 @@ fn scan_from_index(index: &LibraryIndex) -> LocalLibraryScan {
|
||||
/// Scans the local game directory and returns summaries plus a game database.
|
||||
pub async fn scan_local_library(
|
||||
game_dir: impl AsRef<Path>,
|
||||
state_dir: impl AsRef<Path>,
|
||||
catalog: &HashSet<String>,
|
||||
) -> eyre::Result<LocalLibraryScan> {
|
||||
let game_path = game_dir.as_ref();
|
||||
let state_path = state_dir.as_ref();
|
||||
|
||||
let metadata = match tokio::fs::metadata(game_path).await {
|
||||
Ok(metadata) => metadata,
|
||||
@@ -577,7 +585,7 @@ pub async fn scan_local_library(
|
||||
}
|
||||
|
||||
let _index_guard = LIBRARY_INDEX_LOCK.lock().await;
|
||||
let index_path = library_index_path(game_path);
|
||||
let index_path = library_index_path(state_path);
|
||||
let mut index = load_library_index(&index_path).await;
|
||||
let mut seen_ids = HashSet::new();
|
||||
let mut summaries = HashMap::new();
|
||||
@@ -636,12 +644,14 @@ pub async fn scan_local_library(
|
||||
/// Rescans a single game root through the cached index and returns full library state.
|
||||
pub async fn rescan_local_game(
|
||||
game_dir: impl AsRef<Path>,
|
||||
state_dir: impl AsRef<Path>,
|
||||
catalog: &HashSet<String>,
|
||||
game_id: &str,
|
||||
) -> eyre::Result<LocalLibraryScan> {
|
||||
let game_path = game_dir.as_ref();
|
||||
let state_path = state_dir.as_ref();
|
||||
let _index_guard = LIBRARY_INDEX_LOCK.lock().await;
|
||||
let index_path = library_index_path(game_path);
|
||||
let index_path = library_index_path(state_path);
|
||||
let mut index = load_library_index(&index_path).await;
|
||||
|
||||
let update = update_index_for_game(game_path, game_id, catalog, &mut index).await?;
|
||||
@@ -765,6 +775,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn scan_uses_version_ini_and_local_dir_as_independent_state() {
|
||||
let temp = TempDir::new("lanspread-local-games");
|
||||
let state = TempDir::new("lanspread-local-games-state");
|
||||
let catalog = HashSet::from([
|
||||
"ready".to_string(),
|
||||
"local-only".to_string(),
|
||||
@@ -783,7 +794,7 @@ mod tests {
|
||||
b"20250101",
|
||||
);
|
||||
|
||||
let scan = scan_local_library(temp.path(), &catalog)
|
||||
let scan = scan_local_library(temp.path(), state.path(), &catalog)
|
||||
.await
|
||||
.expect("scan should succeed");
|
||||
|
||||
@@ -818,11 +829,12 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn rescan_promotes_installed_only_game_to_ready_when_sentinel_appears() {
|
||||
let temp = TempDir::new("lanspread-local-games");
|
||||
let state = TempDir::new("lanspread-local-games-state");
|
||||
let catalog = HashSet::from(["game".to_string()]);
|
||||
std::fs::create_dir_all(temp.path().join("game").join("local"))
|
||||
.expect("local install dir should be created");
|
||||
|
||||
let first_scan = scan_local_library(temp.path(), &catalog)
|
||||
let first_scan = scan_local_library(temp.path(), state.path(), &catalog)
|
||||
.await
|
||||
.expect("initial scan should succeed");
|
||||
let local_only = first_scan
|
||||
@@ -835,7 +847,7 @@ mod tests {
|
||||
|
||||
write_file(&temp.path().join("game").join("version.ini"), b"20250101");
|
||||
|
||||
let rescan = rescan_local_game(temp.path(), &catalog, "game")
|
||||
let rescan = rescan_local_game(temp.path(), state.path(), &catalog, "game")
|
||||
.await
|
||||
.expect("rescan should succeed");
|
||||
let ready = rescan
|
||||
@@ -851,11 +863,12 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn concurrent_rescans_preserve_both_index_updates() {
|
||||
let temp = TempDir::new("lanspread-local-games-concurrent");
|
||||
let state = TempDir::new("lanspread-local-games-state");
|
||||
let catalog = HashSet::from(["game-a".to_string(), "game-b".to_string()]);
|
||||
write_file(&temp.path().join("game-a").join("version.ini"), b"20250101");
|
||||
write_file(&temp.path().join("game-b").join("version.ini"), b"20250101");
|
||||
|
||||
let initial = scan_local_library(temp.path(), &catalog)
|
||||
let initial = scan_local_library(temp.path(), state.path(), &catalog)
|
||||
.await
|
||||
.expect("initial scan should succeed");
|
||||
assert_eq!(initial.revision, 1);
|
||||
@@ -864,13 +877,13 @@ mod tests {
|
||||
write_file(&temp.path().join("game-b").join("game-b.eti"), b"archive-b");
|
||||
|
||||
let (scan_a, scan_b) = tokio::join!(
|
||||
rescan_local_game(temp.path(), &catalog, "game-a"),
|
||||
rescan_local_game(temp.path(), &catalog, "game-b")
|
||||
rescan_local_game(temp.path(), state.path(), &catalog, "game-a"),
|
||||
rescan_local_game(temp.path(), state.path(), &catalog, "game-b")
|
||||
);
|
||||
scan_a.expect("game-a rescan should succeed");
|
||||
scan_b.expect("game-b rescan should succeed");
|
||||
|
||||
let index = load_library_index(&library_index_path(temp.path())).await;
|
||||
let index = load_library_index(&library_index_path(state.path())).await;
|
||||
assert_eq!(index.revision, 3);
|
||||
let game_a = index
|
||||
.games
|
||||
|
||||
Reference in New Issue
Block a user