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:
@@ -341,6 +341,7 @@ pub async fn handle_download_game_files_command(
|
||||
}
|
||||
end_download_operation(&ctx_clone, &tx_notify_ui_clone, &download_id).await;
|
||||
download_state_guard.disarm();
|
||||
send_download_finished(&tx_notify_ui_clone, &download_id);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -354,6 +355,8 @@ pub async fn handle_download_game_files_command(
|
||||
.await
|
||||
{
|
||||
clear_active_download(&ctx_clone, &download_id).await;
|
||||
send_download_finished(&tx_notify_ui_clone, &download_id);
|
||||
download_state_guard.disarm();
|
||||
run_started_install_operation(
|
||||
&ctx_clone,
|
||||
&tx_notify_ui_clone,
|
||||
@@ -362,7 +365,9 @@ pub async fn handle_download_game_files_command(
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
clear_active_download(&ctx_clone, &download_id).await;
|
||||
end_download_operation(&ctx_clone, &tx_notify_ui_clone, &download_id).await;
|
||||
download_state_guard.disarm();
|
||||
send_download_finished(&tx_notify_ui_clone, &download_id);
|
||||
}
|
||||
} else {
|
||||
if let Err(err) = refresh_local_game_for_ending_operation(
|
||||
@@ -375,8 +380,9 @@ pub async fn handle_download_game_files_command(
|
||||
log::error!("Failed to refresh local library after download: {err}");
|
||||
}
|
||||
end_download_operation(&ctx_clone, &tx_notify_ui_clone, &download_id).await;
|
||||
download_state_guard.disarm();
|
||||
send_download_finished(&tx_notify_ui_clone, &download_id);
|
||||
}
|
||||
download_state_guard.disarm();
|
||||
}
|
||||
Err(e) => {
|
||||
if let Err(refresh_err) = refresh_local_game_for_ending_operation(
|
||||
@@ -393,6 +399,7 @@ pub async fn handle_download_game_files_command(
|
||||
end_download_operation(&ctx_clone, &tx_notify_ui_clone, &download_id).await;
|
||||
download_state_guard.disarm();
|
||||
log::error!("Download failed for {download_id}: {e}");
|
||||
send_download_failed(&tx_notify_ui_clone, &download_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -538,12 +545,13 @@ async fn run_started_install_operation(
|
||||
},
|
||||
);
|
||||
|
||||
let state_dir = ctx.state_dir.as_ref();
|
||||
match operation {
|
||||
InstallOperation::Installing => {
|
||||
install::install(&game_root, &id, ctx.unpacker.clone()).await
|
||||
install::install(&game_root, state_dir, &id, ctx.unpacker.clone()).await
|
||||
}
|
||||
InstallOperation::Updating => {
|
||||
install::update(&game_root, &id, ctx.unpacker.clone()).await
|
||||
install::update(&game_root, state_dir, &id, ctx.unpacker.clone()).await
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -601,7 +609,7 @@ async fn run_uninstall_operation(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerE
|
||||
PeerEvent::UninstallGameBegin { id: id.clone() },
|
||||
);
|
||||
|
||||
install::uninstall(&game_root, &id).await
|
||||
install::uninstall(&game_root, ctx.state_dir.as_ref(), &id).await
|
||||
};
|
||||
|
||||
match result {
|
||||
@@ -767,6 +775,18 @@ async fn clear_active_download(ctx: &Ctx, id: &str) {
|
||||
ctx.active_downloads.write().await.remove(id);
|
||||
}
|
||||
|
||||
fn send_download_finished(tx_notify_ui: &UnboundedSender<PeerEvent>, id: &str) {
|
||||
if let Err(err) = tx_notify_ui.send(PeerEvent::DownloadGameFilesFinished { id: id.into() }) {
|
||||
log::error!("Failed to send DownloadGameFilesFinished event: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn send_download_failed(tx_notify_ui: &UnboundedSender<PeerEvent>, id: &str) {
|
||||
if let Err(err) = tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed { id: id.into() }) {
|
||||
log::error!("Failed to send DownloadGameFilesFailed event: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn end_download_operation(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEvent>, id: &str) {
|
||||
end_operation(ctx, tx_notify_ui, id).await;
|
||||
clear_active_download(ctx, id).await;
|
||||
@@ -845,7 +865,7 @@ async fn load_local_library_with_policy(
|
||||
) -> eyre::Result<()> {
|
||||
let game_dir = { ctx.game_dir.read().await.clone() };
|
||||
let active_ids = active_operation_ids(ctx).await;
|
||||
install::recover_on_startup(&game_dir, &active_ids).await?;
|
||||
install::recover_on_startup(&game_dir, ctx.state_dir.as_ref(), &active_ids).await?;
|
||||
scan_and_announce_local_library(ctx, tx_notify_ui, &game_dir, event_policy).await
|
||||
}
|
||||
|
||||
@@ -870,7 +890,7 @@ async fn scan_and_announce_local_library(
|
||||
event_policy: LocalLibraryEventPolicy,
|
||||
) -> eyre::Result<()> {
|
||||
let catalog = ctx.catalog.read().await.clone();
|
||||
let scan = scan_local_library(game_dir, &catalog).await?;
|
||||
let scan = scan_local_library(game_dir, ctx.state_dir.as_ref(), &catalog).await?;
|
||||
update_and_announce_games_with_policy(ctx, tx_notify_ui, scan, event_policy, None).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -884,7 +904,7 @@ async fn refresh_local_game_for_ending_operation(
|
||||
) -> eyre::Result<()> {
|
||||
let game_dir = { ctx.game_dir.read().await.clone() };
|
||||
let catalog = ctx.catalog.read().await.clone();
|
||||
let scan = rescan_local_game(&game_dir, &catalog, id).await?;
|
||||
let scan = rescan_local_game(&game_dir, ctx.state_dir.as_ref(), &catalog, id).await?;
|
||||
update_and_announce_games_with_policy(
|
||||
ctx,
|
||||
tx_notify_ui,
|
||||
@@ -1068,7 +1088,8 @@ mod tests {
|
||||
Ctx::new(
|
||||
Arc::new(RwLock::new(PeerGameDB::new())),
|
||||
"peer".to_string(),
|
||||
game_dir,
|
||||
game_dir.clone(),
|
||||
game_dir.join(".test-state"),
|
||||
Arc::new(FakeUnpacker),
|
||||
CancellationToken::new(),
|
||||
TaskTracker::new(),
|
||||
@@ -1332,7 +1353,7 @@ mod tests {
|
||||
.insert("game".to_string(), OperationKind::Installing);
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let catalog = ctx.catalog.read().await.clone();
|
||||
let scan = scan_local_library(temp.path(), &catalog)
|
||||
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), &catalog)
|
||||
.await
|
||||
.expect("scan should succeed");
|
||||
|
||||
@@ -1378,13 +1399,13 @@ mod tests {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let catalog = ctx.catalog.read().await.clone();
|
||||
|
||||
let scan = scan_local_library(temp.path(), &catalog)
|
||||
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), &catalog)
|
||||
.await
|
||||
.expect("first scan should succeed");
|
||||
update_and_announce_games(&ctx, &tx, scan).await;
|
||||
assert_local_update(recv_event(&mut rx).await, false, true);
|
||||
|
||||
let scan = scan_local_library(temp.path(), &catalog)
|
||||
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), &catalog)
|
||||
.await
|
||||
.expect("second scan should succeed");
|
||||
update_and_announce_games(&ctx, &tx, scan).await;
|
||||
@@ -1403,7 +1424,7 @@ mod tests {
|
||||
let ctx = test_ctx(temp.path().to_path_buf());
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let catalog = ctx.catalog.read().await.clone();
|
||||
let scan = scan_local_library(temp.path(), &catalog)
|
||||
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), &catalog)
|
||||
.await
|
||||
.expect("initial scan should succeed");
|
||||
update_and_announce_games(&ctx, &tx, scan).await;
|
||||
@@ -1695,7 +1716,7 @@ mod tests {
|
||||
let ctx = test_ctx(temp.path().to_path_buf());
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let catalog = ctx.catalog.read().await.clone();
|
||||
let scan = scan_local_library(temp.path(), &catalog)
|
||||
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), &catalog)
|
||||
.await
|
||||
.expect("initial scan should succeed");
|
||||
update_and_announce_games(&ctx, &tx, scan).await;
|
||||
@@ -1781,7 +1802,7 @@ mod tests {
|
||||
let ctx = test_ctx(current.path().to_path_buf());
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let catalog = ctx.catalog.read().await.clone();
|
||||
let scan = scan_local_library(current.path(), &catalog)
|
||||
let scan = scan_local_library(current.path(), ctx.state_dir.as_ref(), &catalog)
|
||||
.await
|
||||
.expect("initial scan should succeed");
|
||||
update_and_announce_games(&ctx, &tx, scan).await;
|
||||
|
||||
Reference in New Issue
Block a user