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:
Zephyron
2025-07-11 17:01:37 +10:00
parent 5472256a2d
commit 00c3e7aea5
4 changed files with 142 additions and 16 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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"

View File

@@ -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);