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:
@@ -1,10 +1,8 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::fs::File;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
net::SocketAddr,
|
||||
path::{Component, Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::{Arc, OnceLock},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
@@ -18,9 +16,11 @@ use lanspread_peer::{
|
||||
PeerEvent,
|
||||
PeerGameDB,
|
||||
PeerRuntimeHandle,
|
||||
PeerStartOptions,
|
||||
UnpackFuture,
|
||||
Unpacker,
|
||||
start_peer,
|
||||
migrate_legacy_state,
|
||||
start_peer_with_options,
|
||||
};
|
||||
use tauri::{AppHandle, Emitter as _, Manager};
|
||||
use tauri_plugin_shell::{ShellExt, process::Command};
|
||||
@@ -42,6 +42,7 @@ struct LanSpreadState {
|
||||
peer_game_db: Arc<RwLock<PeerGameDB>>,
|
||||
catalog: Arc<RwLock<HashSet<String>>>,
|
||||
unpack_logs: Arc<RwLock<Vec<UnpackLogEntry>>>,
|
||||
state_dir: OnceLock<PathBuf>,
|
||||
}
|
||||
|
||||
struct PeerEventTx(UnboundedSender<PeerEvent>);
|
||||
@@ -109,8 +110,6 @@ async fn get_unpack_logs(
|
||||
Ok(state.inner().unpack_logs.read().await.clone())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const FIRST_START_DONE_FILE: &str = ".softlan_first_start_done";
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
const GAME_SETUP_SCRIPT: &str = "game_setup.cmd";
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
@@ -441,8 +440,23 @@ fn sanitize_username(username: &str) -> String {
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn script_params(script_path: &Path, id: &str, settings: &LaunchSettings) -> String {
|
||||
script_params_with_mode("/c", script_path, id, settings)
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn server_script_params(script_path: &Path, id: &str, settings: &LaunchSettings) -> String {
|
||||
script_params_with_mode("/k", script_path, id, settings)
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn script_params_with_mode(
|
||||
cmd_mode: &str,
|
||||
script_path: &Path,
|
||||
id: &str,
|
||||
settings: &LaunchSettings,
|
||||
) -> String {
|
||||
format!(
|
||||
r#"/d /s /c ""{}" "local" "{}" "{}" "{}"""#,
|
||||
r#"/d /s {cmd_mode} ""{}" "local" "{}" "{}" "{}"""#,
|
||||
script_path.display(),
|
||||
id,
|
||||
settings.language,
|
||||
@@ -487,7 +501,12 @@ async fn get_game_thumbnail(
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn run_as_admin(file: &str, params: &str, dir: &str) -> bool {
|
||||
fn run_as_admin(
|
||||
file: &str,
|
||||
params: &str,
|
||||
dir: &str,
|
||||
show_cmd: windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD,
|
||||
) -> bool {
|
||||
use std::{ffi::OsStr, os::windows::ffi::OsStrExt};
|
||||
|
||||
use windows::{Win32::UI::Shell::ShellExecuteW, core::PCWSTR};
|
||||
@@ -504,7 +523,7 @@ fn run_as_admin(file: &str, params: &str, dir: &str) -> bool {
|
||||
PCWSTR::from_raw(file_wide.as_ptr()),
|
||||
PCWSTR::from_raw(params_wide.as_ptr()),
|
||||
PCWSTR::from_raw(dir_wide.as_ptr()),
|
||||
windows::Win32::UI::WindowsAndMessaging::SW_HIDE,
|
||||
show_cmd,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -540,9 +559,13 @@ async fn run_game_windows(
|
||||
|
||||
let game_setup_bin = game_path.join(GAME_SETUP_SCRIPT);
|
||||
let game_start_bin = game_path.join(GAME_START_SCRIPT);
|
||||
let Some(state_dir) = state.inner().state_dir.get().cloned() else {
|
||||
log::error!("app state directory is not initialized; cannot run game");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let first_start_done_file = game_path.join("local").join(FIRST_START_DONE_FILE);
|
||||
if !first_start_done_file.exists() && game_setup_bin.exists() {
|
||||
let setup_done_file = lanspread_peer::setup_done_path(&state_dir, &id);
|
||||
if !setup_done_file.exists() && game_setup_bin.exists() {
|
||||
if !local_install_is_present(&game_path) {
|
||||
log::warn!(
|
||||
"local install is missing for {}; skipping game_setup",
|
||||
@@ -555,6 +578,7 @@ async fn run_game_windows(
|
||||
"cmd.exe",
|
||||
&script_params(&game_setup_bin, &id, &settings),
|
||||
&game_path.display().to_string(),
|
||||
windows::Win32::UI::WindowsAndMessaging::SW_HIDE,
|
||||
);
|
||||
|
||||
if !result {
|
||||
@@ -562,10 +586,19 @@ async fn run_game_windows(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(e) = File::create(&first_start_done_file) {
|
||||
if let Some(parent) = setup_done_file.parent()
|
||||
&& let Err(e) = std::fs::create_dir_all(parent)
|
||||
{
|
||||
log::error!(
|
||||
"failed to create first-start marker {}: {e}",
|
||||
first_start_done_file.display()
|
||||
"failed to create setup marker directory {}: {e}",
|
||||
parent.display()
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) = std::fs::File::create(&setup_done_file) {
|
||||
log::error!(
|
||||
"failed to create setup marker {}: {e}",
|
||||
setup_done_file.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -575,6 +608,7 @@ async fn run_game_windows(
|
||||
"cmd.exe",
|
||||
&script_params(&game_start_bin, &id, &settings),
|
||||
&game_path.display().to_string(),
|
||||
windows::Win32::UI::WindowsAndMessaging::SW_HIDE,
|
||||
);
|
||||
|
||||
if !result {
|
||||
@@ -646,8 +680,9 @@ async fn start_server_windows(
|
||||
|
||||
let result = run_as_admin(
|
||||
"cmd.exe",
|
||||
&script_params(&server_start_bin, &id, &settings),
|
||||
&server_script_params(&server_start_bin, &id, &settings),
|
||||
&game_path.display().to_string(),
|
||||
windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL,
|
||||
);
|
||||
|
||||
if !result {
|
||||
@@ -850,6 +885,21 @@ async fn update_game_directory(app_handle: tauri::AppHandle, path: String) -> ta
|
||||
}
|
||||
|
||||
let path_changed = current_path != path;
|
||||
let Some(state_dir) = state.state_dir.get().cloned() else {
|
||||
log::error!("app state directory is not initialized; cannot update game directory");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if path_changed || state.peer_ctrl.read().await.is_none() {
|
||||
let migration = migrate_legacy_state(&games_folder, &state_dir).await;
|
||||
if migration.failures > 0 {
|
||||
log::warn!(
|
||||
"Legacy state migration completed with {} failure(s)",
|
||||
migration.failures
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
*state.games_folder.write().await = path;
|
||||
|
||||
ensure_bundled_game_db_loaded(&app_handle).await;
|
||||
@@ -1193,16 +1243,23 @@ async fn ensure_peer_started(app_handle: &AppHandle, games_folder: &Path) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(state_dir) = state.state_dir.get().cloned() else {
|
||||
log::error!("app state directory is not initialized; cannot start peer");
|
||||
return;
|
||||
};
|
||||
let tx_peer_event = app_handle.state::<PeerEventTx>().inner().0.clone();
|
||||
let unpacker = Arc::new(SidecarUnpacker {
|
||||
app_handle: app_handle.clone(),
|
||||
});
|
||||
match start_peer(
|
||||
match start_peer_with_options(
|
||||
games_folder.to_path_buf(),
|
||||
tx_peer_event,
|
||||
state.peer_game_db.clone(),
|
||||
unpacker,
|
||||
state.catalog.clone(),
|
||||
PeerStartOptions {
|
||||
state_dir: Some(state_dir),
|
||||
},
|
||||
) {
|
||||
Ok(handle) => {
|
||||
let sender = handle.sender();
|
||||
@@ -1621,7 +1678,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn script_params_use_common_argument_shape() {
|
||||
let params = script_params(
|
||||
let start_params = script_params(
|
||||
Path::new("C:/Games/My Game")
|
||||
.join(GAME_START_SCRIPT)
|
||||
.as_path(),
|
||||
@@ -1633,9 +1690,25 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
params,
|
||||
start_params,
|
||||
r#"/d /s /c ""C:/Games/My Game/game_start.cmd" "local" "my-game" "en" "Alice"""#
|
||||
);
|
||||
|
||||
let server_params = server_script_params(
|
||||
Path::new("C:/Games/My Game")
|
||||
.join(SERVER_START_SCRIPT)
|
||||
.as_path(),
|
||||
"my-game",
|
||||
&LaunchSettings {
|
||||
language: "en".to_string(),
|
||||
username: "Alice".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
server_params,
|
||||
r#"/d /s /k ""C:/Games/My Game/server_start.cmd" "local" "my-game" "en" "Alice"""#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1743,6 +1816,12 @@ pub fn run() {
|
||||
.manage(LanSpreadState::default())
|
||||
.manage(PeerEventTx(tx_peer_event))
|
||||
.setup(move |app| {
|
||||
let state_dir = app.path().app_data_dir()?;
|
||||
std::fs::create_dir_all(&state_dir)?;
|
||||
let state = app.state::<LanSpreadState>();
|
||||
if state.state_dir.set(state_dir).is_err() {
|
||||
log::warn!("app state directory was already initialized");
|
||||
}
|
||||
spawn_peer_event_loop(app.handle().clone(), rx_peer_event);
|
||||
Ok(())
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user