From 932f31e37bead478d1375ba447d7f02cab9643e4 Mon Sep 17 00:00:00 2001 From: Eladash Date: Sun, 21 Feb 2021 21:55:07 +0200 Subject: [PATCH] Atomic PARAM.SFO writes --- Utilities/File.cpp | 54 +++++++++++++++++++++++++ Utilities/File.h | 16 ++++++++ rpcs3/Emu/Cell/Modules/cellGame.cpp | 12 ++++-- rpcs3/Emu/Cell/Modules/cellSaveData.cpp | 9 ++++- rpcs3/Emu/Cell/lv2/sys_fs.cpp | 7 ++++ rpcs3/Emu/System.cpp | 6 ++- rpcs3/Loader/PSF.cpp | 8 +++- rpcs3/Loader/PSF.h | 2 +- rpcs3/rpcs3qt/emu_settings.cpp | 5 ++- 9 files changed, 108 insertions(+), 11 deletions(-) diff --git a/Utilities/File.cpp b/Utilities/File.cpp index dd50049f0e..7a9980799b 100644 --- a/Utilities/File.cpp +++ b/Utilities/File.cpp @@ -18,6 +18,11 @@ using namespace std::literals::string_literals; #include #include +namespace utils +{ + u64 get_unique_tsc(); +} + static std::unique_ptr to_wchar(const std::string& source) { // String size + null terminator @@ -1915,6 +1920,55 @@ fs::file fs::make_gather(std::vector files) return result; } +fs::pending_file::pending_file(const std::string& path) +{ + do + { + m_path = fmt::format(u8"%s/$%s.%s.tmp", get_parent_dir(path), std::string_view(path).substr(path.find_last_of(fs::delim) + 1), fmt::base57(utils::get_unique_tsc())); + + if (file.open(m_path, fs::create + fs::write + fs::read + fs::excl)) + { + m_dest = path; + break; + } + + m_path.clear(); + } + while (fs::g_tls_error == fs::error::exist); // Only retry if failed due to existing file +} + +fs::pending_file::~pending_file() +{ + file.close(); + + if (!m_path.empty()) + { + fs::remove_file(m_path); + } +} + +bool fs::pending_file::commit(bool overwrite) +{ + if (!file || m_path.empty()) + { + fs::g_tls_error = fs::error::noent; + return false; + } + + // The temporary file's contents must be on disk before rename + file.sync(); + file.close(); + + if (fs::rename(m_path, m_dest, overwrite)) + { + // Disable the destructor + m_path.clear(); + return true; + } + + return false; +} + template<> void fmt_class_string::format(std::string& out, u64 arg) { diff --git a/Utilities/File.h b/Utilities/File.h index bdfbf500f8..d39242bd80 100644 --- a/Utilities/File.h +++ b/Utilities/File.h @@ -602,6 +602,22 @@ namespace fs // Get common cache directory const std::string& get_cache_dir(); + // Unique pending file creation destined to be renamed to the destination file + struct pending_file + { + fs::file file; + + // This is meant to modify files atomically, overwriting is likely + bool commit(bool overwrite = true); + + pending_file(const std::string& path); + ~pending_file(); + + private: + std::string m_path; // Pending file path + std::string m_dest; // Destination file path + }; + // Get real path for comparisons (TODO: investigate std::filesystem::path::compare implementation) std::string escape_path(std::string_view path); diff --git a/rpcs3/Emu/Cell/Modules/cellGame.cpp b/rpcs3/Emu/Cell/Modules/cellGame.cpp index 9b240cb1b6..8c76949bfe 100644 --- a/rpcs3/Emu/Cell/Modules/cellGame.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGame.cpp @@ -666,7 +666,9 @@ error_code cellGameContentPermit(vm::ptr contentInfoPa if (!perm->temp.empty()) { // Create PARAM.SFO - psf::save_object(fs::file(perm->temp + "/PARAM.SFO", fs::rewrite), perm->sfo); + fs::pending_file temp(perm->temp + "/PARAM.SFO"); + temp.file.write(psf::save_object(perm->sfo)); + ensure(temp.commit()); // Make temporary directory persistent (atomically) if (vfs::host::rename(perm->temp, vfs::get(dir), &g_mp_sys_dev_hdd0, false)) @@ -684,7 +686,9 @@ error_code cellGameContentPermit(vm::ptr contentInfoPa else if (perm->can_create) { // Update PARAM.SFO - psf::save_object(fs::file(vfs::get(dir + "/PARAM.SFO"), fs::rewrite), perm->sfo); + fs::pending_file temp(vfs::get(dir + "/PARAM.SFO")); + temp.file.write(psf::save_object(perm->sfo)); + ensure(temp.commit()); } // Cleanup @@ -806,7 +810,9 @@ error_code cellGameDataCheckCreate2(ppu_thread& ppu, u32 version, vm::cptr psf::assign(sfo, fmt::format("TITLE_%02d", i), psf::string(CELL_GAME_SYSP_TITLE_SIZE, setParam->titleLang[i])); } - psf::save_object(fs::file(vfs::get(dir + "/PARAM.SFO"), fs::rewrite), sfo); + fs::pending_file temp(vfs::get(dir + "/PARAM.SFO")); + temp.file.write(psf::save_object(sfo)); + ensure(temp.commit()); } return CELL_OK; diff --git a/rpcs3/Emu/Cell/Modules/cellSaveData.cpp b/rpcs3/Emu/Cell/Modules/cellSaveData.cpp index f003fa37b2..addd5fa9c3 100644 --- a/rpcs3/Emu/Cell/Modules/cellSaveData.cpp +++ b/rpcs3/Emu/Cell/Modules/cellSaveData.cpp @@ -1306,7 +1306,12 @@ static NEVER_INLINE error_code savedata_op(ppu_thread& ppu, u32 operation, u32 v if (!entry.is_directory) { - if (entry.name == "PARAM.SFO" || entry.name == "PARAM.PFD") + if (entry.name == "."sv) + { + continue; + } + + if (entry.name == "PARAM.SFO"sv || entry.name == "PARAM.PFD"sv) { continue; // system files are not included in the file list } @@ -1899,7 +1904,7 @@ static NEVER_INLINE error_code savedata_op(ppu_thread& ppu, u32 operation, u32 v // Write all files in temporary directory auto& fsfo = all_files["PARAM.SFO"]; fsfo = fs::make_stream>(); - psf::save_object(fsfo, psf); + fsfo.write(psf::save_object(psf)); for (auto&& pair : all_files) { diff --git a/rpcs3/Emu/Cell/lv2/sys_fs.cpp b/rpcs3/Emu/Cell/lv2/sys_fs.cpp index f8d6510d8d..122297c658 100644 --- a/rpcs3/Emu/Cell/lv2/sys_fs.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_fs.cpp @@ -768,6 +768,13 @@ error_code sys_fs_opendir(ppu_thread& ppu, vm::cptr path, vm::ptr fd) // Preprocess entries data.back().name = vfs::unescape(data.back().name); + if (!data.back().is_directory && data.back().name == "."sv) + { + // Files hidden from emulation + data.resize(data.size() - 1); + continue; + } + // Add additional entries for split file candidates (while ends with .66600) while (data.back().name.ends_with(".66600")) { diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index e0a872743f..cb753a4ece 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -1299,8 +1299,10 @@ game_boot_result Emulator::Load(const std::string& title_id, bool add_only, bool games[m_title_id] = bdvd_dir; YAML::Emitter out; out << games; - fs::file(fs::get_config_dir() + "/games.yml.tmp", fs::rewrite).write(out.c_str(), out.size()); - fs::rename(fs::get_config_dir() + "/games.yml.tmp", fs::get_config_dir() + "/games.yml", true); + + fs::pending_file temp(fs::get_config_dir() + "/games.yml"); + temp.file.write(out.c_str(), out.size()); + temp.commit(); } else if (m_cat == "1P" && from_hdd0_game) { diff --git a/rpcs3/Loader/PSF.cpp b/rpcs3/Loader/PSF.cpp index 6d0952aa15..a76cee22ed 100644 --- a/rpcs3/Loader/PSF.cpp +++ b/rpcs3/Loader/PSF.cpp @@ -113,6 +113,8 @@ namespace psf return result; } + stream.seek(0); + // Get header header_t header; ensure(stream.read(header)); @@ -186,8 +188,10 @@ namespace psf return result; } - void save_object(const fs::file& stream, const psf::registry& psf) + std::vector save_object(const psf::registry& psf, std::vector&& init) { + fs::file stream = fs::make_stream>(std::move(init)); + std::vector indices; indices.reserve(psf.size()); // Generate indices and calculate key table length @@ -264,6 +268,8 @@ namespace psf fmt::throw_exception("Invalid entry format (key='%s', fmt=0x%x)", entry.first, fmt); } } + + return std::move(static_cast>*>(stream.release().get())->obj); } std::string_view get_string(const registry& psf, const std::string& key, std::string_view def) diff --git a/rpcs3/Loader/PSF.h b/rpcs3/Loader/PSF.h index e353902092..7968b0e71b 100644 --- a/rpcs3/Loader/PSF.h +++ b/rpcs3/Loader/PSF.h @@ -53,7 +53,7 @@ namespace psf registry load_object(const fs::file&); // Convert PSF registry to SFO binary format - void save_object(const fs::file&, const registry&); + std::vector save_object(const registry&, std::vector&& init = std::vector{}); // Get string value or default value std::string_view get_string(const registry& psf, const std::string& key, std::string_view def = ""sv); diff --git a/rpcs3/rpcs3qt/emu_settings.cpp b/rpcs3/rpcs3qt/emu_settings.cpp index 095b291741..12c9b41de1 100644 --- a/rpcs3/rpcs3qt/emu_settings.cpp +++ b/rpcs3/rpcs3qt/emu_settings.cpp @@ -190,8 +190,9 @@ void emu_settings::SaveSettings() } // Save config atomically - fs::file(config_name + ".tmp", fs::rewrite).write(out.c_str(), out.size()); - fs::rename(config_name + ".tmp", config_name, true); + fs::pending_file temp(config_name); + temp.file.write(out.c_str(), out.size()); + temp.commit(); // Check if the running config/title is the same as the edited config/title. if (config_name == g_cfg.name || m_title_id == Emu.GetTitleID())