From 0ce89925a25a1e32d8e558b7fe4ea929d242f874 Mon Sep 17 00:00:00 2001 From: Zephyron Date: Sat, 1 Nov 2025 19:19:02 +1000 Subject: [PATCH] Fix Windows auto updater file locking issue Implement deferred update mechanism using a helper batch script that applies updates after the application exits, avoiding Windows file locking issues. On Windows, the updater now: - Stages update files and creates a helper batch script - Launches the script as a detached process - Exits the application - The script waits for app closure, applies updates, and restarts Citron Linux AppImage updates continue to work as before with the existing method. Signed-off-by: Zephyron --- src/citron/main.cpp | 17 ++++- src/citron/updater/updater_dialog.cpp | 26 +++++++ src/citron/updater/updater_service.cpp | 97 ++++++++++++++++++++++++++ src/citron/updater/updater_service.h | 2 + 4 files changed, 141 insertions(+), 1 deletion(-) diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 4796e99b7..ccb27fd34 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -6069,8 +6069,22 @@ int main(int argc, char* argv[]) { #endif #ifdef CITRON_USE_AUTO_UPDATER - // Check for and apply staged updates before starting the main application std::filesystem::path app_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); + +#ifdef _WIN32 + // On Windows, updates are applied by the helper script after the app exits. + // If we find a staging directory here, it means the helper script failed. + // Clean it up to avoid confusion. + std::filesystem::path staging_path = app_dir / "update_staging"; + if (std::filesystem::exists(staging_path)) { + try { + std::filesystem::remove_all(staging_path); + } catch (...) { + // Ignore cleanup errors + } + } +#else + // On Linux, apply staged updates at startup as before if (Updater::UpdaterService::HasStagedUpdate(app_dir)) { if (Updater::UpdaterService::ApplyStagedUpdate(app_dir)) { // Show a simple message that update was applied @@ -6079,6 +6093,7 @@ int main(int argc, char* argv[]) { } } #endif +#endif #ifdef _WIN32 OverrideWindowsFont(); diff --git a/src/citron/updater/updater_dialog.cpp b/src/citron/updater/updater_dialog.cpp index 29654b63e..5ccd4bf19 100644 --- a/src/citron/updater/updater_dialog.cpp +++ b/src/citron/updater/updater_dialog.cpp @@ -334,6 +334,31 @@ void UpdaterDialog::ShowInstallingState() { void UpdaterDialog::ShowCompletedState() { current_state = State::Completed; + +#ifdef _WIN32 + // On Windows, launch the update helper script and exit immediately + ui->titleLabel->setText(QStringLiteral("Update ready!")); + ui->statusLabel->setText(QStringLiteral("Citron will now restart to apply the update...")); + ui->progressGroup->setVisible(false); + ui->downloadButton->setVisible(false); + ui->cancelButton->setVisible(false); + ui->closeButton->setVisible(false); + ui->restartButton->setVisible(false); + ui->progressBar->setValue(100); + ui->appImageSelectorLabel->setVisible(false); + ui->appImageSelector->setVisible(false); + + // Give the user a moment to see the message + QTimer::singleShot(1500, this, [this]() { + if (updater_service->LaunchUpdateHelper()) { + QApplication::quit(); + } else { + ShowErrorState(); + ui->statusLabel->setText(QStringLiteral("Failed to launch update helper. Please restart Citron manually to apply the update.")); + } + }); +#else + // On Linux, show the restart button as before 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 " @@ -346,6 +371,7 @@ void UpdaterDialog::ShowCompletedState() { ui->progressBar->setValue(100); ui->appImageSelectorLabel->setVisible(false); ui->appImageSelector->setVisible(false); +#endif } void UpdaterDialog::ShowErrorState() { diff --git a/src/citron/updater/updater_service.cpp b/src/citron/updater/updater_service.cpp index 6ea330b2d..7ab46f734 100644 --- a/src/citron/updater/updater_service.cpp +++ b/src/citron/updater/updater_service.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #ifdef CITRON_ENABLE_LIBARCHIVE #include @@ -474,6 +475,13 @@ bool UpdaterService::InstallUpdate(const std::filesystem::path& update_path) { manifest << "UPDATE_TIMESTAMP=" << std::time(nullptr) << "\n"; manifest << "APP_DIRECTORY=" << app_directory.string() << "\n"; } + + // Create the update helper script for deferred update application + if (!CreateUpdateHelperScript(staging_path)) { + LOG_ERROR(Frontend, "Failed to create update helper script"); + return false; + } + LOG_INFO(Frontend, "Update staged successfully."); return true; } catch (const std::exception& e) { @@ -531,6 +539,95 @@ bool UpdaterService::RestoreBackup() { return false; } } + +bool UpdaterService::CreateUpdateHelperScript(const std::filesystem::path& staging_path) { + try { + std::filesystem::path script_path = staging_path / "apply_update.bat"; + std::ofstream script(script_path); + + if (!script.is_open()) { + LOG_ERROR(Frontend, "Failed to create update helper script"); + return false; + } + + // Convert paths to Windows-style paths for the batch script + std::string staging_path_str = staging_path.string(); + std::string app_path_str = app_directory.string(); + std::string exe_path_str = (app_directory / "citron.exe").string(); + + // Replace forward slashes with backslashes + for (auto& ch : staging_path_str) if (ch == '/') ch = '\\'; + for (auto& ch : app_path_str) if (ch == '/') ch = '\\'; + for (auto& ch : exe_path_str) if (ch == '/') ch = '\\'; + + // Write batch script + script << "@echo off\n"; + script << "REM Citron Auto-Updater Helper Script\n"; + script << "REM This script applies staged updates after the main application exits\n\n"; + + script << "echo Waiting for Citron to close...\n"; + script << "timeout /t 3 /nobreak >nul\n\n"; + + script << "echo Applying update...\n"; + script << "xcopy /E /Y /I \"" << staging_path_str << "\" \"" << app_path_str << "\" >nul 2>&1\n\n"; + + script << "if errorlevel 1 (\n"; + script << " echo Update failed. Please restart Citron manually.\n"; + script << " timeout /t 5\n"; + script << " exit /b 1\n"; + script << ")\n\n"; + + script << "echo Update applied successfully!\n"; + script << "timeout /t 1 /nobreak >nul\n\n"; + + script << "echo Restarting Citron...\n"; + script << "start \"\" \"" << exe_path_str << "\"\n\n"; + + script << "REM Clean up staging directory\n"; + script << "rd /s /q \"" << staging_path_str << "\" >nul 2>&1\n\n"; + + script << "REM Delete this script\n"; + script << "del \"%~f0\"\n"; + + script.close(); + + LOG_INFO(Frontend, "Update helper script created: {}", script_path.string()); + return true; + } catch (const std::exception& e) { + LOG_ERROR(Frontend, "Failed to create update helper script: {}", e.what()); + return false; + } +} + +bool UpdaterService::LaunchUpdateHelper() { + try { + std::filesystem::path staging_path = app_directory / "update_staging"; + std::filesystem::path script_path = staging_path / "apply_update.bat"; + + if (!std::filesystem::exists(script_path)) { + LOG_ERROR(Frontend, "Update helper script not found"); + return false; + } + + // Launch the batch script as a detached process + QString script_path_str = QString::fromStdString(script_path.string()); + QStringList arguments; + + // Use cmd.exe to run the batch file in a hidden window + bool launched = QProcess::startDetached("cmd.exe", QStringList() << "/C" << script_path_str); + + if (launched) { + LOG_INFO(Frontend, "Update helper script launched successfully"); + return true; + } else { + LOG_ERROR(Frontend, "Failed to launch update helper script"); + return false; + } + } catch (const std::exception& e) { + LOG_ERROR(Frontend, "Failed to launch update helper: {}", e.what()); + return false; + } +} #endif bool UpdaterService::CleanupFiles() { diff --git a/src/citron/updater/updater_service.h b/src/citron/updater/updater_service.h index e75b9b96f..78e1c3008 100644 --- a/src/citron/updater/updater_service.h +++ b/src/citron/updater/updater_service.h @@ -79,6 +79,8 @@ private: bool InstallUpdate(const std::filesystem::path& update_path); bool CreateBackup(); bool RestoreBackup(); + bool CreateUpdateHelperScript(const std::filesystem::path& staging_path); + bool LaunchUpdateHelper(); #endif bool CleanupFiles(); std::filesystem::path GetTempDirectory() const;