mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-20 02:53:57 +00:00
fix: Implement two-stage update system to handle file-in-use errors
Replace direct file overwriting with a staging-based update mechanism to resolve "file in use" errors during self-updates. **Changes:** - Stage updates to temporary directory instead of direct installation - Apply staged updates on next application startup (before files are loaded) - Add static methods for startup update detection and application - Create update manifest to track staged update metadata - Backup existing files before applying updates - Update UI messaging to reflect staged update workflow **Problem solved:** The previous direct file replacement approach failed when trying to overwrite DLLs and executables that were loaded into memory by the running process. This two-stage approach stages files safely, then applies them on restart when no files are in use. **Workflow:** 1. Download and extract update to staging directory 2. Create manifest with update metadata 3. Show "Update ready" message to user 4. On next app startup: detect staged update, apply it, show success message 5. Clean up staging directory after successful application This ensures reliable self-updates without file access conflicts. Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
@@ -623,35 +623,58 @@ bool UpdaterService::ExtractArchiveWindows(const std::filesystem::path& archive_
|
||||
|
||||
bool UpdaterService::InstallUpdate(const std::filesystem::path& update_path) {
|
||||
try {
|
||||
// Copy all files from update path to application directory
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(update_path)) {
|
||||
// Check if there's a single directory in the update path (common with archives)
|
||||
std::filesystem::path source_path = update_path;
|
||||
|
||||
std::vector<std::filesystem::path> top_level_items;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(update_path)) {
|
||||
top_level_items.push_back(entry.path());
|
||||
}
|
||||
|
||||
// If there's only one top-level directory, use it as the source
|
||||
if (top_level_items.size() == 1 && std::filesystem::is_directory(top_level_items[0])) {
|
||||
source_path = top_level_items[0];
|
||||
LOG_INFO(Frontend, "Found single directory in archive: {}", source_path.filename().string());
|
||||
}
|
||||
|
||||
// Create a staging directory for the update
|
||||
std::filesystem::path staging_path = app_directory / "update_staging";
|
||||
EnsureDirectoryExists(staging_path);
|
||||
|
||||
// Copy all files to staging directory first (this avoids file-in-use issues)
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(source_path)) {
|
||||
if (cancel_requested.load()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.is_regular_file()) {
|
||||
std::filesystem::path relative_path = std::filesystem::relative(entry.path(), update_path);
|
||||
std::filesystem::path dest_path = app_directory / relative_path;
|
||||
std::filesystem::path relative_path = std::filesystem::relative(entry.path(), source_path);
|
||||
std::filesystem::path staging_dest = staging_path / relative_path;
|
||||
|
||||
// Create destination directory if it doesn't exist
|
||||
std::filesystem::create_directories(dest_path.parent_path());
|
||||
std::filesystem::create_directories(staging_dest.parent_path());
|
||||
|
||||
// Copy file
|
||||
std::filesystem::copy_file(entry.path(), dest_path,
|
||||
// Copy to staging directory
|
||||
std::filesystem::copy_file(entry.path(), staging_dest,
|
||||
std::filesystem::copy_options::overwrite_existing);
|
||||
|
||||
LOG_DEBUG(Frontend, "Installed file: {}", dest_path.string());
|
||||
LOG_DEBUG(Frontend, "Staged file: {} -> {}", entry.path().string(), staging_dest.string());
|
||||
}
|
||||
}
|
||||
|
||||
// Update version file
|
||||
std::filesystem::path version_file = app_directory / CITRON_VERSION_FILE;
|
||||
std::ofstream file(version_file);
|
||||
if (file.is_open()) {
|
||||
file << current_update_info.version;
|
||||
file.close();
|
||||
// Create update manifest for post-restart installation
|
||||
std::filesystem::path manifest_file = staging_path / "update_manifest.txt";
|
||||
std::ofstream manifest(manifest_file);
|
||||
if (manifest.is_open()) {
|
||||
manifest << "UPDATE_VERSION=" << current_update_info.version << "\n";
|
||||
manifest << "UPDATE_TIMESTAMP=" << std::time(nullptr) << "\n";
|
||||
manifest << "APP_DIRECTORY=" << app_directory.string() << "\n";
|
||||
manifest.close();
|
||||
}
|
||||
|
||||
LOG_INFO(Frontend, "Update staged successfully. Files prepared in: {}", staging_path.string());
|
||||
LOG_INFO(Frontend, "Update will be applied after application restart.");
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(Frontend, "Failed to install update: {}", e.what());
|
||||
@@ -791,6 +814,94 @@ bool UpdaterService::EnsureDirectoryExists(const std::filesystem::path& path) co
|
||||
}
|
||||
}
|
||||
|
||||
bool UpdaterService::HasStagedUpdate(const std::filesystem::path& app_directory) {
|
||||
std::filesystem::path staging_path = app_directory / "update_staging";
|
||||
std::filesystem::path manifest_file = staging_path / "update_manifest.txt";
|
||||
|
||||
return std::filesystem::exists(staging_path) &&
|
||||
std::filesystem::exists(manifest_file) &&
|
||||
std::filesystem::is_directory(staging_path);
|
||||
}
|
||||
|
||||
bool UpdaterService::ApplyStagedUpdate(const std::filesystem::path& app_directory) {
|
||||
try {
|
||||
std::filesystem::path staging_path = app_directory / "update_staging";
|
||||
std::filesystem::path manifest_file = staging_path / "update_manifest.txt";
|
||||
|
||||
if (!std::filesystem::exists(staging_path) || !std::filesystem::exists(manifest_file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO(Frontend, "Applying staged update from: {}", staging_path.string());
|
||||
|
||||
// Create backup directory for current files
|
||||
std::filesystem::path backup_path = app_directory / "backup_before_update";
|
||||
if (std::filesystem::exists(backup_path)) {
|
||||
std::filesystem::remove_all(backup_path);
|
||||
}
|
||||
std::filesystem::create_directories(backup_path);
|
||||
|
||||
// Copy files from staging to application directory
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(staging_path)) {
|
||||
if (entry.path().filename() == "update_manifest.txt") {
|
||||
continue; // Skip manifest file
|
||||
}
|
||||
|
||||
if (entry.is_regular_file()) {
|
||||
std::filesystem::path relative_path = std::filesystem::relative(entry.path(), staging_path);
|
||||
std::filesystem::path dest_path = app_directory / relative_path;
|
||||
|
||||
// Backup existing file if it exists
|
||||
if (std::filesystem::exists(dest_path)) {
|
||||
std::filesystem::path backup_dest = backup_path / relative_path;
|
||||
std::filesystem::create_directories(backup_dest.parent_path());
|
||||
std::filesystem::copy_file(dest_path, backup_dest);
|
||||
}
|
||||
|
||||
// Create destination directory and copy new file
|
||||
std::filesystem::create_directories(dest_path.parent_path());
|
||||
std::filesystem::copy_file(entry.path(), dest_path,
|
||||
std::filesystem::copy_options::overwrite_existing);
|
||||
|
||||
LOG_DEBUG(Frontend, "Updated file: {}", dest_path.string());
|
||||
}
|
||||
}
|
||||
|
||||
// Read and apply version from manifest
|
||||
std::ifstream manifest(manifest_file);
|
||||
std::string line;
|
||||
std::string version;
|
||||
|
||||
while (std::getline(manifest, line)) {
|
||||
if (line.starts_with("UPDATE_VERSION=")) {
|
||||
version = line.substr(15); // Remove "UPDATE_VERSION="
|
||||
break;
|
||||
}
|
||||
}
|
||||
manifest.close();
|
||||
|
||||
// Update version file
|
||||
if (!version.empty()) {
|
||||
std::filesystem::path version_file = app_directory / "version.txt";
|
||||
std::ofstream vfile(version_file);
|
||||
if (vfile.is_open()) {
|
||||
vfile << version;
|
||||
vfile.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up staging directory
|
||||
std::filesystem::remove_all(staging_path);
|
||||
|
||||
LOG_INFO(Frontend, "Update applied successfully. Version: {}", version);
|
||||
return true;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(Frontend, "Failed to apply staged update: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Updater
|
||||
|
||||
#include "updater_service.moc"
|
||||
Reference in New Issue
Block a user