#include "update_manager.h" #include "progress_dialog.h" #include "localized.h" #include "rpcs3_version.h" #include "downloader.h" #include "gui_settings.h" #include "Utilities/File.h" #include "Emu/System.h" #include "Crypto/utils.h" #include "util/logs.hpp" #include "util/types.hpp" #include "util/sysinfo.hpp" #include #include #include #include #include #include #include #include #include #if defined(_WIN32) || defined(__APPLE__) #include <7z.h> #include <7zAlloc.h> #include <7zCrc.h> #include <7zFile.h> #endif #if defined(_WIN32) #ifndef NOMINMAX #define NOMINMAX #endif #include #include #ifndef PATH_MAX #define PATH_MAX MAX_PATH #endif #include "Utilities/StrUtil.h" #else #include #include #endif LOG_CHANNEL(update_log, "UPDATER"); update_manager::update_manager(QObject* parent, std::shared_ptr gui_settings) : QObject(parent), m_gui_settings(std::move(gui_settings)) { } void update_manager::check_for_updates(bool automatic, bool check_only, bool auto_accept, QWidget* parent) { update_log.notice("Checking for updates: automatic=%d, check_only=%d, auto_accept=%d", automatic, check_only, auto_accept); m_update_message.clear(); m_changelog.clear(); if (automatic) { // Don't check for updates on local builds if (rpcs3::is_local_build()) { update_log.notice("Skipped automatic update check: this is a local build"); return; } #ifdef __linux__ // Don't check for updates on startup if RPCS3 is not running from an AppImage. if (!::getenv("APPIMAGE")) { update_log.notice("Skipped automatic update check: this is not an AppImage"); return; } #endif } m_parent = parent; m_downloader = new downloader(parent); connect(m_downloader, &downloader::signal_download_error, this, [this, automatic](const QString& /*error*/) { if (!automatic) { QMessageBox::warning(m_parent, tr("Auto-updater"), tr("An error occurred during the auto-updating process.\nCheck the log for more information.")); } }); connect(m_downloader, &downloader::signal_download_finished, this, [this, automatic, check_only, auto_accept](const QByteArray& data) { const bool result_json = handle_json(automatic, check_only, auto_accept, data); if (!result_json) { // The progress dialog is configured to stay open, so we need to close it manually if the download succeeds. m_downloader->close_progress_dialog(); if (!automatic) { QMessageBox::warning(m_parent, tr("Auto-updater"), tr("An error occurred during the auto-updating process.\nCheck the log for more information.")); } } Q_EMIT signal_update_available(result_json && !m_update_message.isEmpty()); }); const utils::OS_version os = utils::get_OS_version(); const std::string url = fmt::format("https://update.rpcs3.net/?api=v3&c=%s&os_type=%s&os_arch=%s&os_version=%i.%i.%i", rpcs3::get_commit_and_hash().second, os.type, os.arch, os.version_major, os.version_minor, os.version_patch); m_downloader->start(url, true, !automatic, tr("Checking For Updates"), true); } bool update_manager::handle_json(bool automatic, bool check_only, bool auto_accept, const QByteArray& data) { update_log.notice("Download of update info finished. automatic=%d, check_only=%d, auto_accept=%d", automatic, check_only, auto_accept); const QJsonObject json_data = QJsonDocument::fromJson(data).object(); const int return_code = json_data["return_code"].toInt(-255); bool hash_found = true; if (return_code < 0) { std::string error_message; switch (return_code) { case -1: error_message = "Hash not found(Custom/PR build)"; break; case -2: error_message = "Server Error - Maintenance Mode"; break; case -3: error_message = "Server Error - Illegal Search"; break; case -255: error_message = "Server Error - Return code not found"; break; default: error_message = "Server Error - Unknown Error"; break; } if (return_code != -1) update_log.error("Update error: %s return code: %d", error_message, return_code); else update_log.warning("Update error: %s return code: %d", error_message, return_code); // If a user clicks "Check for Updates" with a custom build ask him if he's sure he wants to update to latest version if (!automatic && return_code == -1) { hash_found = false; } else { return false; } } const auto& current = json_data["current_build"]; const auto& latest = json_data["latest_build"]; if (!latest.isObject()) { update_log.error("JSON doesn't contain latest_build section"); return false; } QString os; #ifdef _WIN32 os = "windows"; #elif defined(__linux__) os = "linux"; #elif defined(__APPLE__) os = "mac"; #else update_log.error("Your OS isn't currently supported by the auto-updater"); return false; #endif // Check that every bit of info we need is there const auto check_json = [](bool cond, std::string_view msg) -> bool { if (cond) return true; update_log.error("%s", msg); return false; }; if (!(check_json(latest[os].isObject(), fmt::format("Node 'latest_build: %s' not found", os)) && check_json(latest[os]["download"].isString(), fmt::format("Node 'latest_build: %s: download' not found or not a string", os)) && check_json(latest[os]["size"].isDouble(), fmt::format("Node 'latest_build: %s: size' not found or not a double", os)) && check_json(latest[os]["checksum"].isString(), fmt::format("Node 'latest_build: %s: checksum' not found or not a string", os)) && check_json(latest["version"].isString(), "Node 'latest_build: version' not found or not a string") && check_json(latest["datetime"].isString(), "Node 'latest_build: datetime' not found or not a string") ) || (hash_found && !( check_json(current.isObject(), "JSON doesn't contain current_build section") && check_json(current["version"].isString(), "Node 'current_build: datetime' not found or not a string") && check_json(current["datetime"].isString(), "Node 'current_build: version' not found or not a string") ))) { return false; } if (hash_found && return_code == 0) { update_log.success("RPCS3 is up to date!"); m_downloader->close_progress_dialog(); if (!automatic) QMessageBox::information(m_parent, tr("Auto-updater"), tr("Your version is already up to date!")); return true; } // Calculate how old the build is const QString date_fmt = QStringLiteral("yyyy-MM-dd hh:mm:ss"); const QDateTime cur_date = hash_found ? QDateTime::fromString(current["datetime"].toString(), date_fmt) : QDateTime::currentDateTimeUtc(); const QDateTime lts_date = QDateTime::fromString(latest["datetime"].toString(), date_fmt); const QString cur_str = cur_date.toString(date_fmt); const QString lts_str = lts_date.toString(date_fmt); const qint64 diff_msec = cur_date.msecsTo(lts_date); update_log.notice("Current: %s, latest: %s, difference: %lld ms", cur_str, lts_str, diff_msec); const Localized localized; const QString new_version = latest["version"].toString(); m_new_version = new_version.toStdString(); const QString support_message = tr("
You can empower our project at RPCS3 Patreon.
"); if (hash_found) { const QString old_version = current["version"].toString(); m_old_version = old_version.toStdString(); if (diff_msec < 0) { // This usually means that the current version was marked as broken and won't be shipped anymore, so we need to downgrade to avoid certain bugs. m_update_message = tr("A better version of RPCS3 is available!

Current version: %0 (%1)
Better version: %2 (%3)
%4
Do you want to update?") .arg(old_version) .arg(cur_str) .arg(new_version) .arg(lts_str) .arg(support_message); } else { m_update_message = tr("A new version of RPCS3 is available!

Current version: %0 (%1)
Latest version: %2 (%3)
Your version is %4 behind.
%5
Do you want to update?") .arg(old_version) .arg(cur_str) .arg(new_version) .arg(lts_str) .arg(localized.GetVerboseTimeByMs(diff_msec, true)) .arg(support_message); } } else { m_old_version = fmt::format("%s-%s-%s", rpcs3::get_full_branch(), rpcs3::get_branch(), rpcs3::get_version().to_string()); m_update_message = tr("You're currently using a custom or PR build.

Latest version: %0 (%1)
The latest version is %2 old.
%3
Do you want to update to the latest official RPCS3 version?") .arg(new_version) .arg(lts_str) .arg(localized.GetVerboseTimeByMs(std::abs(diff_msec), true)) .arg(support_message); } m_request_url = latest[os]["download"].toString().toStdString(); m_expected_hash = latest[os]["checksum"].toString().toStdString(); m_expected_size = latest[os]["size"].toInt(); if (!m_request_url.starts_with("https://github.com/RPCS3/rpcs3")) { update_log.fatal("Bad url: %s", m_request_url); return false; } update_log.notice("Update found: %s", m_request_url); if (!auto_accept) { if (automatic && m_gui_settings->GetValue(gui::ib_skip_version).toString() == new_version) { update_log.notice("Skipping automatic update notification for version '%s' due to user preference", new_version); m_downloader->close_progress_dialog(); return true; } const auto& changelog = json_data["changelog"]; if (changelog.isArray()) { for (const QJsonValue& changelog_entry : changelog.toArray()) { if (changelog_entry.isObject()) { changelog_data entry; if (QJsonValue version = changelog_entry["version"]; version.isString()) { entry.version = version.toString(); } else { entry.version = tr("N/A"); update_log.notice("JSON changelog entry does not contain a version string."); } if (QJsonValue title = changelog_entry["title"]; title.isString()) { entry.title = title.toString(); } else { entry.title = tr("N/A"); update_log.notice("JSON changelog entry does not contain a title string."); } m_changelog.push_back(entry); } else { update_log.error("JSON changelog entry is not an object."); } } } else if (changelog.isObject()) { update_log.error("JSON changelog is not an array."); } else { update_log.notice("JSON does not contain a changelog section."); } } if (check_only) { update_log.notice("Update postponed. Check only is active"); m_downloader->close_progress_dialog(); return true; } update(auto_accept); return true; } void update_manager::update(bool auto_accept) { update_log.notice("Updating with auto_accept=%d", auto_accept); ensure(m_downloader); if (!auto_accept) { if (m_update_message.isEmpty()) { // This can happen if we abort the check_for_updates download. Just check again in this case. update_log.notice("Aborting update: Update message is empty. Trying again..."); m_downloader->close_progress_dialog(); check_for_updates(false, false, false, m_parent); return; } QString changelog_content; for (const changelog_data& entry : m_changelog) { if (!changelog_content.isEmpty()) changelog_content.append('\n'); changelog_content.append(tr("• %0: %1").arg(entry.version, entry.title)); } QMessageBox mb(QMessageBox::Icon::Question, tr("Update Available"), m_update_message, QMessageBox::Yes | QMessageBox::No, m_downloader->get_progress_dialog() ? m_downloader->get_progress_dialog() : m_parent); mb.setTextFormat(Qt::RichText); mb.setCheckBox(new QCheckBox(tr("Don't show again for this version"))); if (!changelog_content.isEmpty()) { mb.setInformativeText(tr("To see the changelog, please click \"Show Details\".")); mb.setDetailedText(tr("Changelog:\n\n%0").arg(changelog_content)); // Smartass hack to make the unresizeable message box wide enough for the changelog const int changelog_width = QLabel(changelog_content).sizeHint().width(); if (QLabel(m_update_message).sizeHint().width() < changelog_width) { m_update_message += "  "; while (QLabel(m_update_message).sizeHint().width() < changelog_width) { m_update_message += " "; } } mb.setText(m_update_message); } update_log.notice("Asking user for permission to update..."); if (mb.exec() == QMessageBox::No) { update_log.notice("Aborting update: User declined update"); if (mb.checkBox()->isChecked()) { update_log.notice("User requested to skip further automatic update notifications for version '%s'", m_new_version); m_gui_settings->SetValue(gui::ib_skip_version, QString::fromStdString(m_new_version)); } m_downloader->close_progress_dialog(); return; } } if (!Emu.IsStopped()) { update_log.notice("Aborting update: Emulation is running..."); m_downloader->close_progress_dialog(); QMessageBox::warning(m_parent, tr("Auto-updater"), tr("Please stop the emulation before trying to update.")); return; } m_downloader->disconnect(); connect(m_downloader, &downloader::signal_download_error, this, [this](const QString& /*error*/) { QMessageBox::warning(m_parent, tr("Auto-updater"), tr("An error occurred during the auto-updating process.\nCheck the log for more information.")); }); connect(m_downloader, &downloader::signal_download_finished, this, [this, auto_accept](const QByteArray& data) { const bool result_json = handle_rpcs3(data, auto_accept); if (!result_json) { // The progress dialog is configured to stay open, so we need to close it manually if the download succeeds. m_downloader->close_progress_dialog(); QMessageBox::warning(m_parent, tr("Auto-updater"), tr("An error occurred during the auto-updating process.\nCheck the log for more information.")); } Q_EMIT signal_update_available(false); }); update_log.notice("Downloading update..."); m_downloader->start(m_request_url, true, true, tr("Downloading Update"), true, m_expected_size); } bool update_manager::handle_rpcs3(const QByteArray& data, bool auto_accept) { update_log.notice("Download of update file finished. Updating rpcs3 with auto_accept=%d", auto_accept); m_downloader->update_progress_dialog(tr("Updating RPCS3")); if (m_expected_size != static_cast(data.size())) { update_log.error("Download size mismatch: %d expected: %d", data.size(), m_expected_size); return false; } if (const std::string res_hash_string = sha256_get_hash(data.data(), data.size(), false); m_expected_hash != res_hash_string) { update_log.error("Hash mismatch: %s expected: %s", res_hash_string, m_expected_hash); return false; } #if defined(_WIN32) || defined(__APPLE__) // Get executable path const std::string exe_dir = fs::get_executable_dir(); const std::string orig_path = fs::get_executable_path(); #ifdef _WIN32 const std::wstring wchar_orig_path = utf8_to_wchar(orig_path); const std::string tmpfile_path = fs::get_temp_dir() + "\\rpcs3_update.7z"; #else const std::string tmpfile_path = fs::get_temp_dir() + "rpcs3_update.7z"; #endif update_log.notice("Writing temporary update file: %s", tmpfile_path); fs::file tmpfile(tmpfile_path, fs::read + fs::write + fs::create + fs::trunc); if (!tmpfile) { update_log.error("Failed to create temporary file: %s", tmpfile_path); return false; } if (tmpfile.write(data.data(), data.size()) != static_cast(data.size())) { update_log.error("Failed to write temporary file: %s", tmpfile_path); return false; } tmpfile.close(); update_log.notice("Unpacking update file: %s", tmpfile_path); // 7z stuff (most of this stuff is from 7z Util sample and has been reworked to be more stl friendly) CFileInStream archiveStream{}; CLookToRead2 lookStream{}; CSzArEx db; UInt16 temp_u16[PATH_MAX]; u8 temp_u8[PATH_MAX]; const usz kInputBufSize = static_cast(1u << 18u); const ISzAlloc g_Alloc = {SzAlloc, SzFree}; ISzAlloc allocImp = g_Alloc; ISzAlloc allocTempImp = g_Alloc; if (const WRes res = InFile_Open(&archiveStream.file, tmpfile_path.c_str())) { update_log.error("Failed to open temporary storage file: '%s' (error=%d)", tmpfile_path, static_cast(res)); return false; } FileInStream_CreateVTable(&archiveStream); LookToRead2_CreateVTable(&lookStream, False); SRes res = SZ_OK; lookStream.buf = static_cast(ISzAlloc_Alloc(&allocImp, kInputBufSize)); if (!lookStream.buf) { res = SZ_ERROR_MEM; } else { lookStream.bufSize = kInputBufSize; lookStream.realStream = &archiveStream.vt; } CrcGenerateTable(); SzArEx_Init(&db); auto error_free7z = [&]() { SzArEx_Free(&db, &allocImp); ISzAlloc_Free(&allocImp, lookStream.buf); const WRes res2 = File_Close(&archiveStream.file); if (res2) update_log.warning("7z failed to close file (error=%d)", static_cast(res2)); switch (res) { case SZ_OK: break; case SZ_ERROR_UNSUPPORTED: update_log.error("7z decoder doesn't support this archive"); break; case SZ_ERROR_MEM: update_log.error("7z decoder failed to allocate memory"); break; case SZ_ERROR_CRC: update_log.error("7z decoder CRC error"); break; default: update_log.error("7z decoder error: %d", static_cast(res)); break; } }; if (res != SZ_OK) { error_free7z(); return false; } res = SzArEx_Open(&db, &lookStream.vt, &allocImp, &allocTempImp); if (res != SZ_OK) { error_free7z(); return false; } UInt32 blockIndex = 0xFFFFFFFF; Byte* outBuffer = nullptr; usz outBufferSize = 0; #ifdef _WIN32 // Create temp folder for moving active files const std::string tmp_folder = exe_dir + "rpcs3_old/"; #else // Create temp folder for extracting the new app const std::string tmp_folder = fs::get_temp_dir() + "rpcs3_new/"; #endif fs::create_dir(tmp_folder); for (UInt32 i = 0; i < db.NumFiles; i++) { usz offset = 0; usz outSizeProcessed = 0; const bool isDir = SzArEx_IsDir(&db, i); [[maybe_unused]] const DWORD attribs = SzBitWithVals_Check(&db.Attribs, i) ? db.Attribs.Vals[i] : 0; #ifdef _WIN32 // This is commented out for now as we shouldn't need it and symlinks // aren't well supported on Windows. Left in case it is needed in the future. // const bool is_symlink = (attribs & FILE_ATTRIBUTE_REPARSE_POINT) != 0; const bool is_symlink = false; #else const DWORD permissions = (attribs >> 16) & (S_IRWXU | S_IRWXG | S_IRWXO); const bool is_symlink = (attribs & FILE_ATTRIBUTE_UNIX_EXTENSION) != 0 && S_ISLNK(attribs >> 16); #endif const usz len = SzArEx_GetFileNameUtf16(&db, i, nullptr); if (len >= PATH_MAX) { update_log.error("7z decoder error: filename longer or equal to PATH_MAX"); error_free7z(); return false; } SzArEx_GetFileNameUtf16(&db, i, temp_u16); memset(temp_u8, 0, sizeof(temp_u8)); // Simplistic conversion to UTF-8 for (usz index = 0; index < len; index++) { if (temp_u16[index] > 0xFF) { update_log.error("7z decoder error: Failed to convert UTF-16 to UTF-8"); error_free7z(); return false; } temp_u8[index] = static_cast(temp_u16[index]); } temp_u8[len] = 0; const std::string archived_name = std::string(reinterpret_cast(temp_u8)); #ifdef __APPLE__ const std::string name = tmp_folder + archived_name; #else const std::string name = exe_dir + archived_name; #endif if (!isDir) { res = SzArEx_Extract(&db, &lookStream.vt, i, &blockIndex, &outBuffer, &outBufferSize, &offset, &outSizeProcessed, &allocImp, &allocTempImp); if (res != SZ_OK) break; } if (const usz pos = name.find_last_of(fs::delim); pos != umax) { update_log.trace("Creating path: %s", name.substr(0, pos)); fs::create_path(name.substr(0, pos)); } if (isDir) { update_log.trace("Creating dir: %s", name); fs::create_dir(name); continue; } if (is_symlink) { const std::string link_target(reinterpret_cast(outBuffer + offset), outSizeProcessed); update_log.trace("Creating symbolic link: %s -> %s", name, link_target); fs::create_symlink(name, link_target); continue; } fs::file outfile(name, fs::read + fs::write + fs::create + fs::trunc); if (!outfile) { // File failed to open, probably because in use, rename existing file and try again const auto pos = name.find_last_of(fs::delim); std::string filename; if (pos == umax) filename = name; else filename = name.substr(pos + 1); // Moving to temp is not an option on windows as it will fail if the disk is different // So we create a folder in config dir and move stuff there const std::string rename_target = tmp_folder + filename; update_log.trace("Renaming %s to %s", name, rename_target); if (!fs::rename(name, rename_target, true)) { update_log.error("Failed to rename %s to %s", name, rename_target); res = SZ_ERROR_FAIL; break; } outfile.open(name, fs::read + fs::write + fs::create + fs::trunc); if (!outfile) { update_log.error("Can not open output file %s", name); res = SZ_ERROR_FAIL; break; } } if (outfile.write(outBuffer + offset, outSizeProcessed) != outSizeProcessed) { update_log.error("Can not write output file: %s", name); res = SZ_ERROR_FAIL; break; } outfile.close(); #ifndef _WIN32 // Apply correct file permissions. chmod(name.c_str(), permissions); #endif } error_free7z(); if (res) return false; update_log.success("Update successful!"); #else std::string replace_path = fs::get_executable_path(); if (replace_path.empty()) { return false; } // Move the appimage/exe and replace with new appimage const std::string move_dest = replace_path + "_old"; if (!fs::rename(replace_path, move_dest, true)) { // Simply log error for now update_log.error("Failed to move old AppImage file: %s (%s)", replace_path, fs::g_tls_error); } fs::file new_appimage(replace_path, fs::read + fs::write + fs::create + fs::trunc); if (!new_appimage) { update_log.error("Failed to create new AppImage file: %s (%s)", replace_path, fs::g_tls_error); return false; } if (new_appimage.write(data.data(), data.size()) != data.size() + 0ull) { update_log.error("Failed to write new AppImage file: %s", replace_path); return false; } if (fchmod(new_appimage.get_handle(), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) == -1) { update_log.error("Failed to chmod rwxrxrx %s (%s)", replace_path, strerror(errno)); return false; } new_appimage.close(); update_log.success("Successfully updated %s!", replace_path); #endif m_downloader->close_progress_dialog(); // Add new version to log file if (fs::file update_file{fs::get_config_dir() + "update_history.log", fs::create + fs::write + fs::append}) { const std::string update_time = QDateTime::currentDateTime().toString("yyyy/MM/dd hh:mm:ss").toStdString(); const std::string entry = fmt::format("%s: Updated from \"%s\" to \"%s\"", update_time, m_old_version, m_new_version); update_file.write(fmt::format("%s\n", entry)); update_log.notice("Added entry '%s' to update_history.log", entry); } else { update_log.error("Failed to append version to update_history.log"); } if (!auto_accept) { m_gui_settings->ShowInfoBox(tr("Auto-updater"), tr("Update successful!
RPCS3 will now restart.
"), gui::ib_restart_hint, m_parent); m_gui_settings->sync(); // Make sure to sync before terminating RPCS3 } Emu.GracefulShutdown(false); Emu.CleanUp(); #ifdef _WIN32 update_log.notice("Relaunching %s with _wexecl", wchar_to_utf8(wchar_orig_path)); const int ret = _wexecl(wchar_orig_path.data(), wchar_orig_path.data(), L"--updating", nullptr); #elif defined(__APPLE__) // Execute helper script to replace the app and relaunch const std::string helper_script = fmt::format("%s/Contents/Resources/update_helper.sh", orig_path); const std::string extracted_app = fmt::format("%s/RPCS3.app", tmp_folder); update_log.notice("Executing update helper script: '%s %s %s'", helper_script, extracted_app, orig_path); const int ret = execl(helper_script.c_str(), helper_script.c_str(), extracted_app.c_str(), orig_path.c_str(), nullptr); #else // execv is used for compatibility with checkrt update_log.notice("Relaunching %s with execv", replace_path); const char * const params[3] = { replace_path.c_str(), "--updating", nullptr }; const int ret = execv(replace_path.c_str(), const_cast(¶ms[0])); #endif if (ret == -1) { update_log.error("Relaunching failed with result: %d(%s)", ret, strerror(errno)); return false; } return true; }