automatically clear out any corrupted files (#708)
Removes non-empty all-NUL files from the game directory before content is materialized, so installations bricked by corrupted files recover on the next launch.
This commit is contained in:
parent
3d0c880096
commit
275abd13b7
3 changed files with 147 additions and 0 deletions
143
packages/oneclient_core/src/game/heal.rs
Normal file
143
packages/oneclient_core/src/game/heal.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
const PROBE_LEN: usize = 8 * 1024;
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug")]
|
||||||
|
pub async fn clear_zeroed_files(game_dir: &Path) -> usize {
|
||||||
|
let mut cleared = 0usize;
|
||||||
|
let mut stack: Vec<PathBuf> = vec![game_dir.to_path_buf()];
|
||||||
|
|
||||||
|
while let Some(dir) = stack.pop() {
|
||||||
|
let Ok(mut entries) = polyio::read_dir(&dir).await else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||||
|
let Ok(file_type) = entry.file_type().await else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if file_type.is_dir() {
|
||||||
|
stack.push(path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !file_type.is_file() || !is_zeroed(&path).await {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match polyio::remove_file(&path).await {
|
||||||
|
Ok(()) => {
|
||||||
|
cleared += 1;
|
||||||
|
tracing::warn!(
|
||||||
|
file = %path.display(),
|
||||||
|
"removed zero-filled file left by an unclean shutdown"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => tracing::warn!(
|
||||||
|
file = %path.display(),
|
||||||
|
error = %err,
|
||||||
|
"failed to remove zero-filled file"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = vec![0u8; PROBE_LEN];
|
||||||
|
let mut seen_any = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let read = match file.read(&mut buf).await {
|
||||||
|
Ok(0) => break,
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if buf[..read].iter().any(|&b| b != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen_any = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ mod analytics;
|
||||||
mod error;
|
mod error;
|
||||||
#[cfg(any(target_os = "linux", test))]
|
#[cfg(any(target_os = "linux", test))]
|
||||||
mod gpu;
|
mod gpu;
|
||||||
|
mod heal;
|
||||||
mod launch;
|
mod launch;
|
||||||
mod log_replay;
|
mod log_replay;
|
||||||
mod process;
|
mod process;
|
||||||
|
|
@ -23,6 +24,7 @@ pub mod diagnosis;
|
||||||
|
|
||||||
pub use diagnosis::{CrashDiagnosis, diagnose};
|
pub use diagnosis::{CrashDiagnosis, diagnose};
|
||||||
pub use error::GameError;
|
pub use error::GameError;
|
||||||
|
pub use heal::clear_zeroed_files;
|
||||||
pub use launch::{LaunchedGame, is_running, launch_cluster, offer_repair};
|
pub use launch::{LaunchedGame, is_running, launch_cluster, offer_repair};
|
||||||
pub use process::{
|
pub use process::{
|
||||||
GameProcess, GameProcessManager, is_process_alive, kill_process, process_start_time,
|
GameProcess, GameProcessManager, is_process_alive, kill_process, process_start_time,
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@ pub async fn materialize_content(
|
||||||
let dedicated = cluster.uses_dedicated_dir();
|
let dedicated = cluster.uses_dedicated_dir();
|
||||||
polyio::create_dir_all(game_dir).await.ok();
|
polyio::create_dir_all(game_dir).await.ok();
|
||||||
|
|
||||||
|
crate::game::heal::clear_zeroed_files(game_dir).await;
|
||||||
|
|
||||||
import_manual_content(services, cluster, game_dir).await;
|
import_manual_content(services, cluster, game_dir).await;
|
||||||
|
|
||||||
// Before the folder is built not after handing the game several enabled
|
// Before the folder is built not after handing the game several enabled
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue