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 <zephyron@citron-emu.org>
This commit is contained in:
Zephyron
2025-11-01 19:19:02 +10:00
parent a462e66927
commit 0ce89925a2
4 changed files with 141 additions and 1 deletions

View File

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

View File

@@ -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() {

View File

@@ -22,6 +22,7 @@
#include <QCoreApplication>
#include <QSslSocket>
#include <QCryptographicHash>
#include <QProcess>
#ifdef CITRON_ENABLE_LIBARCHIVE
#include <archive.h>
@@ -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() {

View File

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