fix: long launches caused by anti-corruption mechanism
This commit is contained in:
parent
068ea1beaa
commit
f55f816342
1 changed files with 91 additions and 82 deletions
|
|
@ -1,35 +1,88 @@
|
|||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
use oneclient_common::domain::ContentType;
|
||||
|
||||
const PROBE_LEN: usize = 8 * 1024;
|
||||
const PROBE_LEN: usize = 4096;
|
||||
|
||||
const MAX_PROBE_BYTES: u64 = 32 * 1024 * 1024;
|
||||
|
||||
const BULK_NAMES: [&[u8]; 4] = [b"cache", b"repo", b"backup", b"logs"];
|
||||
|
||||
const NEVER_PROBED: [&str; 8] = [
|
||||
"db", "sqlite", "sqlite3", "wal", "shm", "journal", "lock", "idx",
|
||||
];
|
||||
|
||||
const SWEPT_DIRS: [&str; 1] = ["config"];
|
||||
|
||||
const SWEPT_CONTENT: [(ContentType, bool); 3] = [
|
||||
(ContentType::Mod, true),
|
||||
(ContentType::ResourcePack, false),
|
||||
(ContentType::Shader, false),
|
||||
];
|
||||
|
||||
#[tracing::instrument(level = "debug")]
|
||||
pub async fn clear_zeroed_files(game_dir: &Path) -> usize {
|
||||
let game_dir = game_dir.to_path_buf();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut cleared = sweep_dir(&game_dir, false);
|
||||
|
||||
for dir in SWEPT_DIRS {
|
||||
cleared += sweep_dir(&game_dir.join(dir), true);
|
||||
}
|
||||
|
||||
for (content_type, recurse) in SWEPT_CONTENT {
|
||||
cleared += sweep_dir(&game_dir.join(content_type.folder_name()), recurse);
|
||||
}
|
||||
|
||||
if cleared > 0 {
|
||||
tracing::info!(
|
||||
cleared,
|
||||
game_dir = %game_dir.display(),
|
||||
"cleared zero-filled files; affected mods will regenerate defaults"
|
||||
);
|
||||
}
|
||||
|
||||
cleared
|
||||
})
|
||||
.await
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn sweep_dir(root: &Path, recurse: bool) -> usize {
|
||||
let mut cleared = 0usize;
|
||||
let mut stack: Vec<PathBuf> = vec![game_dir.to_path_buf()];
|
||||
let mut stack: Vec<PathBuf> = vec![root.to_path_buf()];
|
||||
|
||||
while let Some(dir) = stack.pop() {
|
||||
let Ok(mut entries) = polyio::read_dir(&dir).await else {
|
||||
let Ok(entries) = fs::read_dir(&dir) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
let Ok(file_type) = entry.file_type().await else {
|
||||
for entry in entries.flatten() {
|
||||
let Ok(file_type) = entry.file_type() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let path = entry.path();
|
||||
|
||||
if names_bulk_data(&path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if file_type.is_dir() {
|
||||
stack.push(path);
|
||||
if recurse {
|
||||
stack.push(path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if !file_type.is_file() || !is_zeroed(&path).await {
|
||||
if !file_type.is_file() || !worth_probing(&path) || !is_zeroed(&path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match polyio::remove_file(&path).await {
|
||||
match fs::remove_file(&path) {
|
||||
Ok(()) => {
|
||||
cleared += 1;
|
||||
tracing::warn!(
|
||||
|
|
@ -46,27 +99,39 @@ pub async fn clear_zeroed_files(game_dir: &Path) -> usize {
|
|||
}
|
||||
}
|
||||
|
||||
if cleared > 0 {
|
||||
tracing::info!(
|
||||
cleared,
|
||||
game_dir = %game_dir.display(),
|
||||
"cleared zero-filled files; affected mods will regenerate defaults"
|
||||
);
|
||||
}
|
||||
|
||||
cleared
|
||||
}
|
||||
|
||||
async fn is_zeroed(path: &Path) -> bool {
|
||||
let Ok(mut file) = tokio::fs::File::open(path).await else {
|
||||
fn names_bulk_data(path: &Path) -> bool {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.is_some_and(|name| {
|
||||
let name = name.as_bytes();
|
||||
|
||||
BULK_NAMES.iter().any(|needle| {
|
||||
name.windows(needle.len())
|
||||
.any(|window| window.eq_ignore_ascii_case(needle))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn worth_probing(path: &Path) -> bool {
|
||||
!path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.is_some_and(|ext| NEVER_PROBED.iter().any(|skip| ext.eq_ignore_ascii_case(skip)))
|
||||
}
|
||||
|
||||
fn is_zeroed(path: &Path) -> bool {
|
||||
let Ok(mut file) = fs::File::open(path) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut buf = vec![0u8; PROBE_LEN];
|
||||
let mut seen_any = false;
|
||||
let mut buf = [0u8; PROBE_LEN];
|
||||
let mut read_so_far = 0u64;
|
||||
|
||||
loop {
|
||||
let read = match file.read(&mut buf).await {
|
||||
let read = match file.read(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => n,
|
||||
Err(_) => return false,
|
||||
|
|
@ -76,68 +141,12 @@ async fn is_zeroed(path: &Path) -> bool {
|
|||
return false;
|
||||
}
|
||||
|
||||
seen_any = true;
|
||||
}
|
||||
read_so_far += read as u64;
|
||||
|
||||
seen_any
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn write(path: &Path, bytes: &[u8]) {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).unwrap();
|
||||
if read_so_far > MAX_PROBE_BYTES {
|
||||
return false;
|
||||
}
|
||||
std::fs::write(path, bytes).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clears_only_non_empty_all_nul_files() {
|
||||
let dir = polyio::testing::ScratchDir::new("heal-zeroed");
|
||||
let root = dir.join("game");
|
||||
|
||||
let zeroed = root.join("config").join("sodium-options.json");
|
||||
let partly = root.join("config").join("half.json");
|
||||
let good = root.join("options.txt");
|
||||
let empty = root.join(".dedicated_directory");
|
||||
let nested = root.join("config").join("deep").join("nested.toml");
|
||||
|
||||
write(&zeroed, &[0u8; 818]);
|
||||
write(&partly, &[&[0u8; 4096][..], b"{}"].concat());
|
||||
write(&good, b"fov:70\n");
|
||||
write(&empty, b"");
|
||||
write(&nested, &[0u8; 3]);
|
||||
|
||||
assert_eq!(clear_zeroed_files(&root).await, 2);
|
||||
|
||||
assert!(!zeroed.exists());
|
||||
assert!(!nested.exists(), "recursion must reach nested directories");
|
||||
assert!(partly.exists());
|
||||
assert!(good.exists());
|
||||
assert!(empty.exists(), "empty marker files are not corruption");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn does_not_descend_linked_directories() {
|
||||
let dir = polyio::testing::ScratchDir::new("heal-symlink");
|
||||
let root = dir.join("game");
|
||||
let outside = dir.join("launcher-logs");
|
||||
|
||||
let victim = outside.join("latest.log");
|
||||
write(&victim, &[0u8; 64]);
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
std::os::unix::fs::symlink(&outside, root.join("logs")).unwrap();
|
||||
|
||||
assert_eq!(clear_zeroed_files(&root).await, 0);
|
||||
assert!(victim.exists());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_directory_is_not_an_error() {
|
||||
let dir = polyio::testing::ScratchDir::new("heal-missing");
|
||||
assert_eq!(clear_zeroed_files(&dir.join("nope")).await, 0);
|
||||
}
|
||||
read_so_far > 0
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue