From 00c3e7aea595d49d5629eb7528344940e347a78c Mon Sep 17 00:00:00 2001 From: Zephyron Date: Fri, 11 Jul 2025 17:01:37 +1000 Subject: [PATCH] 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 --- src/citron/main.cpp | 11 ++ src/citron/updater/updater_dialog.cpp | 4 +- src/citron/updater/updater_service.cpp | 139 ++++++++++++++++++++++--- src/citron/updater/updater_service.h | 4 + 4 files changed, 142 insertions(+), 16 deletions(-) diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 2331756a9..4c93a3187 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -162,6 +162,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "citron/startup_checks.h" #include "citron/uisettings.h" #include "citron/updater/updater_dialog.h" +#include "citron/updater/updater_service.h" #include "citron/util/clickable_label.h" #include "citron/vk_device_info.h" @@ -5321,6 +5322,16 @@ int main(int argc, char* 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 OverrideWindowsFont(); #endif diff --git a/src/citron/updater/updater_dialog.cpp b/src/citron/updater/updater_dialog.cpp index 51d5555ef..59290522e 100644 --- a/src/citron/updater/updater_dialog.cpp +++ b/src/citron/updater/updater_dialog.cpp @@ -271,8 +271,8 @@ void UpdaterDialog::ShowInstallingState() { void UpdaterDialog::ShowCompletedState() { current_state = State::Completed; - ui->titleLabel->setText(QStringLiteral("Update completed!")); - ui->statusLabel->setText(QStringLiteral("The update has been installed successfully. Please restart Citron to use the new version.")); + ui->titleLabel->setText(QStringLiteral("Update ready!")); + 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); diff --git a/src/citron/updater/updater_service.cpp b/src/citron/updater/updater_service.cpp index 85a5b0618..7c84cf825 100644 --- a/src/citron/updater/updater_service.cpp +++ b/src/citron/updater/updater_service.cpp @@ -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 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" \ No newline at end of file diff --git a/src/citron/updater/updater_service.h b/src/citron/updater/updater_service.h index 223a9a394..b1a405269 100644 --- a/src/citron/updater/updater_service.h +++ b/src/citron/updater/updater_service.h @@ -59,6 +59,10 @@ public: // Check if update is in progress 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: void UpdateCheckCompleted(bool has_update, const UpdateInfo& update_info); void UpdateDownloadProgress(int percentage, qint64 bytes_received, qint64 bytes_total);