From 038dc9472526a8f8f3f70f2dc71cf103454b37a9 Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:12:52 +0200 Subject: [PATCH] fix(core): disallow launching multiple instances so fast --- packages/oneclient_core/src/game/error.rs | 5 +++++ packages/oneclient_core/src/game/launch.rs | 24 +++++++++++++++++++-- packages/oneclient_core/src/game/process.rs | 11 ++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/oneclient_core/src/game/error.rs b/packages/oneclient_core/src/game/error.rs index 8f5bd229..2a2234ad 100644 --- a/packages/oneclient_core/src/game/error.rs +++ b/packages/oneclient_core/src/game/error.rs @@ -33,6 +33,11 @@ pub enum GameError { #[error("another cluster ({0}) is already running in the same directory")] DirectoryInUse(i64), + #[error( + "{0} is already running in the shared game directory; close it before launching another cluster" + )] + SharedDirectoryBusy(String), + #[error("failed to spawn the game process: {0}")] Spawn(String), } diff --git a/packages/oneclient_core/src/game/launch.rs b/packages/oneclient_core/src/game/launch.rs index 49258132..2da6e4eb 100644 --- a/packages/oneclient_core/src/game/launch.rs +++ b/packages/oneclient_core/src/game/launch.rs @@ -60,12 +60,22 @@ pub async fn launch_cluster( let existing = state.clusters.get(cluster_id).await?; let game_dir = existing.game_dir()?; + let dedicated = existing.uses_dedicated_dir(); + + // Every cluster without a dedicated folder plays out of the same shared + // directory, so a second game there would materialize its content over the + // running one's. `dir_in_use_by` waves a cluster past its own session, which + // parallel launches make reachable, so the directory is checked outright. + if !dedicated && let Some(other) = state.games.dir_in_use(&game_dir) { + tracing::warn!(cluster_id, other, "shared game dir busy; refusing launch"); + let name = running_cluster_name(state, other).await; + return Err(GameError::SharedDirectoryBusy(name).into()); + } + if let Some(other) = state.games.dir_in_use_by(&game_dir, cluster_id) { return Err(GameError::DirectoryInUse(other).into()); } - let dedicated = existing.uses_dedicated_dir(); - let progress = GroupedProgressSession::start( &state.services.events, format!("Launching {}", existing.name), @@ -406,6 +416,16 @@ pub async fn launch_cluster( Ok(LaunchedGame { cluster_id, pid }) } +/// Names the cluster in the user's way for a refusal they have to act on. +/// Falls back to the id, which is still better than no answer at all. +async fn running_cluster_name(state: &Arc, cluster_id: i64) -> String { + state + .clusters + .get(cluster_id) + .await + .map_or_else(|_| format!("Cluster {cluster_id}"), |cluster| cluster.name) +} + /// Cut the game loose from the launcher's process group / console, so signals /// aimed at the launcher (a terminal Ctrl-C, a console window closing) don't /// reach the game as collateral. diff --git a/packages/oneclient_core/src/game/process.rs b/packages/oneclient_core/src/game/process.rs index 802ed471..335fc674 100644 --- a/packages/oneclient_core/src/game/process.rs +++ b/packages/oneclient_core/src/game/process.rs @@ -118,6 +118,17 @@ impl GameProcessManager { self.dirs.lock().unwrap().insert(cluster_id, dir); } + /// Like [`Self::dir_in_use_by`], but counts a cluster's own session too. The + /// shared game directory holds one game at a time, whoever it belongs to. + pub fn dir_in_use(&self, dir: &Path) -> Option { + self.dirs + .lock() + .unwrap() + .iter() + .find(|(_, d)| d.as_path() == dir) + .map(|(id, _)| *id) + } + pub fn dir_in_use_by(&self, dir: &Path, exclude: i64) -> Option { self.dirs .lock()