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:
@@ -162,6 +162,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
|
|||||||
#include "citron/startup_checks.h"
|
#include "citron/startup_checks.h"
|
||||||
#include "citron/uisettings.h"
|
#include "citron/uisettings.h"
|
||||||
#include "citron/updater/updater_dialog.h"
|
#include "citron/updater/updater_dialog.h"
|
||||||
|
#include "citron/updater/updater_service.h"
|
||||||
#include "citron/util/clickable_label.h"
|
#include "citron/util/clickable_label.h"
|
||||||
#include "citron/vk_device_info.h"
|
#include "citron/vk_device_info.h"
|
||||||
|
|
||||||
@@ -5321,6 +5322,16 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
|
|
||||||
|
// Check for and apply staged updates before starting the main application
|
||||||
|
std::filesystem::path app_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString());
|
||||||
|
if (Updater::UpdaterService::HasStagedUpdate(app_dir)) {
|
||||||
|
if (Updater::UpdaterService::ApplyStagedUpdate(app_dir)) {
|
||||||
|
// Show a simple message that update was applied
|
||||||
|
QMessageBox::information(nullptr, QObject::tr("Update Applied"),
|
||||||
|
QObject::tr("Citron has been updated successfully!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
OverrideWindowsFont();
|
OverrideWindowsFont();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -271,8 +271,8 @@ void UpdaterDialog::ShowInstallingState() {
|
|||||||
void UpdaterDialog::ShowCompletedState() {
|
void UpdaterDialog::ShowCompletedState() {
|
||||||
current_state = State::Completed;
|
current_state = State::Completed;
|
||||||
|
|
||||||
ui->titleLabel->setText(QStringLiteral("Update completed!"));
|
ui->titleLabel->setText(QStringLiteral("Update ready!"));
|
||||||
ui->statusLabel->setText(QStringLiteral("The update has been installed successfully. Please restart Citron to use the new version."));
|
ui->statusLabel->setText(QStringLiteral("The update has been downloaded and prepared successfully. The update will be applied when you restart Citron."));
|
||||||
|
|
||||||
ui->progressGroup->setVisible(false);
|
ui->progressGroup->setVisible(false);
|
||||||
|
|
||||||
|
|||||||
@@ -623,35 +623,58 @@ bool UpdaterService::ExtractArchiveWindows(const std::filesystem::path& archive_
|
|||||||
|
|
||||||
bool UpdaterService::InstallUpdate(const std::filesystem::path& update_path) {
|
bool UpdaterService::InstallUpdate(const std::filesystem::path& update_path) {
|
||||||
try {
|
try {
|
||||||
// Copy all files from update path to application directory
|
// Check if there's a single directory in the update path (common with archives)
|
||||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(update_path)) {
|
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()) {
|
if (cancel_requested.load()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.is_regular_file()) {
|
if (entry.is_regular_file()) {
|
||||||
std::filesystem::path relative_path = std::filesystem::relative(entry.path(), update_path);
|
std::filesystem::path relative_path = std::filesystem::relative(entry.path(), source_path);
|
||||||
std::filesystem::path dest_path = app_directory / relative_path;
|
std::filesystem::path staging_dest = staging_path / relative_path;
|
||||||
|
|
||||||
// Create destination directory if it doesn't exist
|
// 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
|
// Copy to staging directory
|
||||||
std::filesystem::copy_file(entry.path(), dest_path,
|
std::filesystem::copy_file(entry.path(), staging_dest,
|
||||||
std::filesystem::copy_options::overwrite_existing);
|
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
|
// Create update manifest for post-restart installation
|
||||||
std::filesystem::path version_file = app_directory / CITRON_VERSION_FILE;
|
std::filesystem::path manifest_file = staging_path / "update_manifest.txt";
|
||||||
std::ofstream file(version_file);
|
std::ofstream manifest(manifest_file);
|
||||||
if (file.is_open()) {
|
if (manifest.is_open()) {
|
||||||
file << current_update_info.version;
|
manifest << "UPDATE_VERSION=" << current_update_info.version << "\n";
|
||||||
file.close();
|
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;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
LOG_ERROR(Frontend, "Failed to install update: {}", e.what());
|
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
|
} // namespace Updater
|
||||||
|
|
||||||
#include "updater_service.moc"
|
#include "updater_service.moc"
|
||||||
@@ -59,6 +59,10 @@ public:
|
|||||||
// Check if update is in progress
|
// Check if update is in progress
|
||||||
bool IsUpdateInProgress() const;
|
bool IsUpdateInProgress() const;
|
||||||
|
|
||||||
|
// Static methods for startup update handling
|
||||||
|
static bool HasStagedUpdate(const std::filesystem::path& app_directory);
|
||||||
|
static bool ApplyStagedUpdate(const std::filesystem::path& app_directory);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void UpdateCheckCompleted(bool has_update, const UpdateInfo& update_info);
|
void UpdateCheckCompleted(bool has_update, const UpdateInfo& update_info);
|
||||||
void UpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total);
|
void UpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total);
|
||||||
|
|||||||
Reference in New Issue
Block a user