From 98d140664639cffc1b48167adb2dff3d24c12624 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Fri, 27 Dec 2024 16:04:40 +0100 Subject: [PATCH] Implement session.lock Lock session.lock so other processes know that we currently read and write to the level. This is vanilla behavior. Test: Tested to start the a new server while on was locking the file, and got `Failed to lock level: AlreadyLocked("session.lock")` as expected --- pumpkin-world/Cargo.toml | 2 ++ pumpkin-world/src/level.rs | 9 +++++++++ pumpkin-world/src/lib.rs | 1 + pumpkin-world/src/lock/anvil.rs | 32 ++++++++++++++++++++++++++++++++ pumpkin-world/src/lock/mod.rs | 18 ++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 pumpkin-world/src/lock/anvil.rs create mode 100644 pumpkin-world/src/lock/mod.rs diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index 495ee3ff..1c7eacd8 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -25,6 +25,8 @@ dashmap = "6.1.0" flate2 = "1.0" lz4 = "1.28.0" +file-guard = "0.2.0" + enum_dispatch = "0.3.13" fastnbt = { git = "https://github.com/owengage/fastnbt.git" } diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 930363ec..ede2d616 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -14,6 +14,7 @@ use crate::{ anvil::AnvilChunkReader, ChunkData, ChunkParsingError, ChunkReader, ChunkReadingError, }, generation::{get_world_gen, Seed, WorldGenerator}, + lock::{anvil::AnvilLevelLocker, LevelLocker}, world_info::{anvil::AnvilLevelInfo, LevelData, WorldInfoReader, WorldInfoWriter}, }; @@ -35,6 +36,9 @@ pub struct Level { chunk_watchers: Arc, usize>>, chunk_reader: Arc, world_gen: Arc, + // Gets unlocked when dropped + // TODO: Make this a trait + _locker: Arc, } #[derive(Clone)] @@ -55,6 +59,10 @@ impl Level { region_folder, }; + // if we fail to lock, lets crash ???. maybe not the best soultion when we have a large server with many worlds and one is locked. + // So TODO + let locker = AnvilLevelLocker::look(&level_folder).expect("Failed to lock level"); + // TODO: Load info correctly based on world format type let level_info = AnvilLevelInfo .read_world_info(&level_folder) @@ -71,6 +79,7 @@ impl Level { loaded_chunks: Arc::new(DashMap::new()), chunk_watchers: Arc::new(DashMap::new()), level_info, + _locker: Arc::new(locker), } } diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index 8e26b106..ec23b151 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -16,6 +16,7 @@ pub mod dimension; mod generation; pub mod item; pub mod level; +mod lock; pub mod world_info; pub const WORLD_HEIGHT: usize = 384; pub const WORLD_LOWEST_Y: i16 = -64; diff --git a/pumpkin-world/src/lock/anvil.rs b/pumpkin-world/src/lock/anvil.rs new file mode 100644 index 00000000..7864cf71 --- /dev/null +++ b/pumpkin-world/src/lock/anvil.rs @@ -0,0 +1,32 @@ +use file_guard::{FileGuard, Lock}; + +use super::{LevelLocker, LockError}; + +use std::{fs::File, io::Write, sync::Arc}; + +pub struct AnvilLevelLocker { + _lock: Option>>, +} + +const SESSION_LOCK_FILE_NAME: &str = "session.lock"; + +const SNOWMAN: &[u8] = "☃".as_bytes(); + +impl LevelLocker for AnvilLevelLocker { + fn look(folder: &crate::level::LevelFolder) -> Result { + let file_path = folder.root_folder.join(SESSION_LOCK_FILE_NAME); + let mut file = File::options() + .create(true) + .truncate(false) + .write(true) + .open(file_path) + .unwrap(); + // im not joking, mojang writes a snowman into the lock file + file.write_all(SNOWMAN) + .map_err(|_| LockError::FailedWrite)?; + let file_arc = Arc::new(file); + let lock = file_guard::try_lock(file_arc, Lock::Exclusive, 0, 1) + .map_err(|_| LockError::AlreadyLocked(SESSION_LOCK_FILE_NAME.to_string()))?; + Ok(Self { _lock: Some(lock) }) + } +} diff --git a/pumpkin-world/src/lock/mod.rs b/pumpkin-world/src/lock/mod.rs new file mode 100644 index 00000000..bf4b262f --- /dev/null +++ b/pumpkin-world/src/lock/mod.rs @@ -0,0 +1,18 @@ +use thiserror::Error; + +use crate::level::LevelFolder; + +pub mod anvil; + +// Gets unlocked when dropped +pub trait LevelLocker: Send + Sync { + fn look(folder: &LevelFolder) -> Result; +} + +#[derive(Error, Debug)] +pub enum LockError { + #[error("Oh no, Level is already locked by {0}")] + AlreadyLocked(String), + #[error("Failed to write into lock file")] + FailedWrite, +}