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
This commit is contained in:
Alexander Medvedev 2024-12-27 16:04:40 +01:00
parent 37372986b0
commit 98d1406646
5 changed files with 62 additions and 0 deletions

View File

@ -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" }

View File

@ -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<DashMap<Vector2<i32>, usize>>,
chunk_reader: Arc<dyn ChunkReader>,
world_gen: Arc<dyn WorldGenerator>,
// Gets unlocked when dropped
// TODO: Make this a trait
_locker: Arc<AnvilLevelLocker>,
}
#[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),
}
}

View File

@ -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;

View File

@ -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<FileGuard<Arc<File>>>,
}
const SESSION_LOCK_FILE_NAME: &str = "session.lock";
const SNOWMAN: &[u8] = "☃".as_bytes();
impl LevelLocker<Self> for AnvilLevelLocker {
fn look(folder: &crate::level::LevelFolder) -> Result<Self, LockError> {
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) })
}
}

View File

@ -0,0 +1,18 @@
use thiserror::Error;
use crate::level::LevelFolder;
pub mod anvil;
// Gets unlocked when dropped
pub trait LevelLocker<T>: Send + Sync {
fn look(folder: &LevelFolder) -> Result<T, LockError>;
}
#[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,
}