From a1cd67ca111437db680fa3560a19a8a34d085926 Mon Sep 17 00:00:00 2001 From: Exzap <13877693+Exzap@users.noreply.github.com> Date: Tue, 1 Sep 2026 07:29:24 +0200 Subject: [PATCH] UI: Refactor auto-updater and make it more robust --- src/CMakeLists.txt | 10 - src/Common/unix/FileStream_unix.cpp | 55 ++ src/Common/unix/FileStream_unix.h | 3 + src/Common/windows/FileStream_win32.cpp | 55 ++ src/Common/windows/FileStream_win32.h | 3 + src/gui/wxgui/CemuApp.cpp | 29 + src/gui/wxgui/CemuApp.h | 3 + src/gui/wxgui/CemuUpdateWindow.cpp | 924 +++++++++++-------- src/gui/wxgui/CemuUpdateWindow.h | 23 +- src/gui/wxgui/ChecksumTool.cpp | 21 +- src/gui/wxgui/DownloadGraphicPacksWindow.cpp | 12 +- src/gui/wxgui/MainWindow.cpp | 1 - src/gui/wxgui/MainWindow.h | 1 + src/main.cpp | 39 +- src/resource/update.sh | 8 - src/util/CMakeLists.txt | 1 + src/util/helpers/ZipArchive.h | 138 +++ 17 files changed, 879 insertions(+), 447 deletions(-) delete mode 100755 src/resource/update.sh create mode 100644 src/util/helpers/ZipArchive.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e8373f9d..d8378c3a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -142,10 +142,8 @@ if (MACOS_BUNDLE) endif() endif() - set(UPDATE_SH_PATH "${CMAKE_SOURCE_DIR}/src/resource/update.sh") set(APP_BUNDLE_DIR "$") set(FRAMEWORKS_DIR "${APP_BUNDLE_DIR}/Contents/Frameworks") - set(RESOURCES_DIR "${APP_BUNDLE_DIR}/Contents/Resources") if (ENABLE_VULKAN) add_custom_command(TARGET CemuBin POST_BUILD @@ -155,14 +153,6 @@ if (MACOS_BUNDLE) ) endif() - add_custom_command(TARGET CemuBin POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - "${UPDATE_SH_PATH}" - "${RESOURCES_DIR}/update.sh" - COMMAND chmod 755 - "${RESOURCES_DIR}/update.sh" - ) - if (ENABLE_LIBUSB) add_custom_command(TARGET CemuBin POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy diff --git a/src/Common/unix/FileStream_unix.cpp b/src/Common/unix/FileStream_unix.cpp index 53880527..b54d3ef6 100644 --- a/src/Common/unix/FileStream_unix.cpp +++ b/src/Common/unix/FileStream_unix.cpp @@ -242,3 +242,58 @@ void FileStream::SyncReadWriteSeek(bool nextOpIsWrite) m_prevOperationWasWrite = nextOpIsWrite; } + +bool FileStream::WriteFileAtomic(const fs::path& path, std::span fileData, bool allowTargetFileRename) +{ + std::error_code ec; + fs::path altPath = path; + altPath.replace_extension( _utf8ToPath(_pathToUtf8(altPath.extension()).append("_tmp"))); + FileStream* fsAlt = FileStream::createFile2(altPath); + if (!fsAlt) + return false; + if (fsAlt->writeData(fileData.data(), fileData.size()) != fileData.size()) + { + delete fsAlt; + fs::remove(altPath, ec); + return false; + } + fsAlt->Flush(); + delete fsAlt; + fs::rename(altPath, path, ec); + if (ec) + { + // the target file might be in use, try to rename it if allowed + // this isn't strictly atomic + if (allowTargetFileRename) + { + fs::path backupPath = path; + backupPath.replace_extension( _utf8ToPath(_pathToUtf8(backupPath.extension()).append(".backup"))); + std::error_code ec; + fs::rename(path, backupPath, ec); + if (ec) + { + cemuLog_log(LogType::Force, "Failed to atomically replace file {}, error {}", _pathToUtf8(path), ec.message()); + fs::remove(altPath, ec); + return false; + } + // target file renamed, try to replace it again + fs::rename(altPath, path, ec); + if (ec) + { + cemuLog_log(LogType::Force, "Failed to atomically replace file {}, error {}", _pathToUtf8(path), ec.message()); + fs::rename(backupPath, path, ec); // try to undo rename + if (ec) + cemuLog_log(LogType::Force, "Failed to restore original file {}, error {}", _pathToUtf8(path), ec.message()); + fs::remove(altPath, ec); + return false; + } + } + else if (ec) + { + cemuLog_log(LogType::Force, "Failed to atomically replace file {}, error {}", _pathToUtf8(path), ec.message()); + fs::remove(altPath, ec); + return false; + } + } + return true; +} \ No newline at end of file diff --git a/src/Common/unix/FileStream_unix.h b/src/Common/unix/FileStream_unix.h index 0a2fa7ed..29261ddd 100644 --- a/src/Common/unix/FileStream_unix.h +++ b/src/Common/unix/FileStream_unix.h @@ -47,6 +47,9 @@ class FileStream ~FileStream(); FileStream() {}; + // atomic helper + static bool WriteFileAtomic(const fs::path& path, std::span fileData, bool allowTargetFileRename = false); + private: void SyncReadWriteSeek(bool nextOpIsWrite); FileStream(const fs::path& path, bool isOpen, bool isWriteable); diff --git a/src/Common/windows/FileStream_win32.cpp b/src/Common/windows/FileStream_win32.cpp index 2ae1247f..5fe4a6b2 100644 --- a/src/Common/windows/FileStream_win32.cpp +++ b/src/Common/windows/FileStream_win32.cpp @@ -185,3 +185,58 @@ FileStream::FileStream(HANDLE hFile) m_hFile = hFile; m_isValid = true; } + +bool FileStream::WriteFileAtomic(const fs::path& path, std::span fileData, bool allowTargetFileRename) +{ + std::error_code ec; + fs::path altPath = path; + altPath.replace_extension( _utf8ToPath(_pathToUtf8(altPath.extension()).append("_tmp"))); + FileStream* fsAlt = FileStream::createFile2(altPath); + if (!fsAlt) + return false; + if (fsAlt->writeData(fileData.data(), fileData.size()) != fileData.size()) + { + delete fsAlt; + fs::remove(altPath, ec); + return false; + } + // only on unix: fsAlt->Flush(); + delete fsAlt; + fs::rename(altPath, path, ec); + if (ec) + { + // the target file might be in use, try to rename it if allowed + // this isn't strictly atomic + if (allowTargetFileRename) + { + fs::path backupPath = path; + backupPath.replace_extension( _utf8ToPath(_pathToUtf8(backupPath.extension()).append(".backup"))); + std::error_code ec; + fs::rename(path, backupPath, ec); + if (ec) + { + cemuLog_log(LogType::Force, "Failed to atomically replace file {}, error {}", _pathToUtf8(path), ec.message()); + fs::remove(altPath, ec); + return false; + } + // target file renamed, try to replace it again + fs::rename(altPath, path, ec); + if (ec) + { + cemuLog_log(LogType::Force, "Failed to atomically replace file {}, error {}", _pathToUtf8(path), ec.message()); + fs::rename(backupPath, path, ec); // try to undo rename + if (ec) + cemuLog_log(LogType::Force, "Failed to restore original file {}, error {}", _pathToUtf8(path), ec.message()); + fs::remove(altPath, ec); + return false; + } + } + else if (ec) + { + cemuLog_log(LogType::Force, "Failed to atomically replace file {}, error {}", _pathToUtf8(path), ec.message()); + fs::remove(altPath, ec); + return false; + } + } + return true; +} diff --git a/src/Common/windows/FileStream_win32.h b/src/Common/windows/FileStream_win32.h index a0b1b30a..cb8466b2 100644 --- a/src/Common/windows/FileStream_win32.h +++ b/src/Common/windows/FileStream_win32.h @@ -45,6 +45,9 @@ class FileStream ~FileStream(); FileStream() = default; + // atomic helper + static bool WriteFileAtomic(const fs::path& path, std::span fileData, bool allowTargetFileRename = false); + private: FileStream(HANDLE hFile); diff --git a/src/gui/wxgui/CemuApp.cpp b/src/gui/wxgui/CemuApp.cpp index 46958aed..98a3fc40 100644 --- a/src/gui/wxgui/CemuApp.cpp +++ b/src/gui/wxgui/CemuApp.cpp @@ -408,6 +408,25 @@ int CemuApp::OnExit() #if BOOST_OS_MACOS SDLControllerProvider::ShutdownSDL(); #endif + // handle restart if requested + if (m_restartExecutable.has_value() && !m_restartExecutable->empty() && fs::exists(*m_restartExecutable)) + { + fs::path restartPath = *m_restartExecutable; +#if BOOST_OS_WINDOWS + PROCESS_INFORMATION pi{}; + STARTUPINFOW si{}; + si.cb = sizeof(si); + std::wstring cmdline; + cmdline = L"\"" + boost::nowide::widen(_pathToUtf8(restartPath)) + L"\""; + CreateProcessW(nullptr, (wchar_t*)cmdline.c_str(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi); +#elif BOOST_OS_LINUX + std::string appPath = _pathToUtf8(restartPath); + execlp(appPath.c_str(), appPath.c_str(), (char *)NULL); +#elif BOOST_OS_MACOS + std::string appPath = _pathToUtf8(restartPath); + execlp(appPath.c_str(), appPath.c_str(), (char *)NULL); +#endif + } #if BOOST_OS_WINDOWS ExitProcess(retValue); #else @@ -647,3 +666,13 @@ void CemuApp::ActivateApp(wxActivateEvent& event) g_window_info.app_active = event.GetActive(); event.Skip(); } + +void CemuApp::RequestRestart(fs::path executablePath) +{ + m_restartExecutable = executablePath; + CallAfter([this, executablePath = std::move(executablePath)]() mutable + { + if (m_mainFrame && !m_mainFrame->IsBeingDeleted()) + m_mainFrame->Close(true); + }); +} diff --git a/src/gui/wxgui/CemuApp.h b/src/gui/wxgui/CemuApp.h index 7735ef74..978123dd 100644 --- a/src/gui/wxgui/CemuApp.h +++ b/src/gui/wxgui/CemuApp.h @@ -23,6 +23,8 @@ public: static void InitializeNewMLCOrFail(fs::path mlc); static void InitializeExistingMLCOrFail(fs::path mlc); + + void RequestRestart(fs::path executablePath); private: void LocalizeUI(wxLanguage languageToUse); @@ -39,6 +41,7 @@ private: wxLocale m_locale; std::vector m_availableTranslations; + std::optional m_restartExecutable; }; wxDECLARE_APP(CemuApp); diff --git a/src/gui/wxgui/CemuUpdateWindow.cpp b/src/gui/wxgui/CemuUpdateWindow.cpp index 53674a58..2b1183c0 100644 --- a/src/gui/wxgui/CemuUpdateWindow.cpp +++ b/src/gui/wxgui/CemuUpdateWindow.cpp @@ -3,9 +3,11 @@ #include "Common/version.h" #include "util/helpers/helpers.h" #include "util/helpers/SystemException.h" +#include "util/helpers/ZipArchive.h" #include "config/ActiveSettings.h" #include "Common/FileStream.h" #include "wxCemuConfig.h" +#include "CemuApp.h" #include #include @@ -19,8 +21,8 @@ #endif #include -#include #include +#include wxDECLARE_EVENT(wxEVT_RESULT, wxCommandEvent); @@ -88,7 +90,7 @@ size_t CemuUpdateWindow::WriteStringCallback(char* ptr, size_t size, size_t nmem return size * nmemb; }; -std::string _curlUrlEscape(CURL* curl, const std::string& input) +static std::string CurlUrlEscape(CURL* curl, const std::string& input) { char* escapedStr = curl_easy_escape(curl, input.c_str(), input.size()); std::string r(escapedStr); @@ -96,41 +98,117 @@ std::string _curlUrlEscape(CURL* curl, const std::string& input) return r; } -std::string _curlUrlUnescape(CURL* curl, std::string_view input) +static std::string CurlUrlUnescape(CURL* curl, std::string_view input) { int decodedLen = 0; - const char* decoded = curl_easy_unescape(curl, input.data(), input.size(), &decodedLen); - return std::string(decoded, decodedLen); + char* decoded = curl_easy_unescape(curl, input.data(), input.size(), &decodedLen); + std::string r(decoded, decodedLen); + curl_free(decoded); + return r; +} + +static std::string GenerateSecureRandomString() +{ + std::array bytes{}; + if (RAND_bytes(bytes.data(), bytes.size()) != 1) + return ""; + std::string result; + result.reserve(21); + for (unsigned char byte : bytes) + { + result.push_back('a' + (byte % 26)); + } + return result; +} + +static bool HexToBytes(const std::string& hex, std::vector& dataOut) +{ + if (hex.size() % 2) + return false; + dataOut.clear(); + dataOut.reserve(hex.size() / 2); + for (size_t i = 0; i < hex.size(); i += 2) + { + unsigned int x; + auto [p, ec] = std::from_chars(hex.data() + i, hex.data() + i + 2, x, 16); + if (ec != std::errc{} || p != hex.data() + i + 2) + return false; + dataOut.push_back(static_cast(x)); + } + return true; +} + +static bool VerifyUpdateResponseSignature(std::string_view message, std::string signatureAsHex) +{ + std::vector signatureData; + if (!HexToBytes(signatureAsHex, signatureData)) + return false; + EVP_PKEY* key = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, (const unsigned char*)"\xa0\x73\xda\x45\x12\x11\xd6\xe0\x2d\x7b\x1a\xb3\x69\xfd\x6c\xc5\x99\xd3\xd3\x1e\xe7\x9d\x57\x89\x2a\xe6\xe8\x40\x54\x7c\xc5\xbd", 32); + if (!key) + return false; + EVP_MD_CTX* context = EVP_MD_CTX_new(); + if (!context) + { + EVP_PKEY_free(key); + return false; + } + bool valid = EVP_DigestVerifyInit(context, nullptr, nullptr, nullptr, key) == 1 && + EVP_DigestVerify(context,signatureData.data(),signatureData.size(), reinterpret_cast(message.data()), message.size()) == 1; + EVP_MD_CTX_free(context); + EVP_PKEY_free(key); + return valid; +} + +// returns empty string for platforms which cant auto-update or dont have support for it yet +static std::string GetPlatformUpdateIdentifier() +{ + std::string identifier; +#if BOOST_OS_LINUX + const char* appImagePath = std::getenv("APPIMAGE"); + if (appImagePath && *appImagePath) + identifier.append("linux_appimage"); + else + return ""; +#elif BOOST_OS_WINDOWS + identifier.append("windows"); +#elif BOOST_OS_MACOS + identifier.append("macos_bundle"); +#elif BOOST_OS_BSD + return ""; // BSD users must update from source +#else + return ""; +#endif +#if defined(__aarch64__) + identifier.append("_aarch64"); +#elif defined(ARCH_X86_64) + identifier.append("_x86_64"); +#else + return ""; +#endif + return identifier; } // returns true if update is available and sets output parameters -bool CemuUpdateWindow::QueryUpdateInfo(std::string& downloadUrlOut, std::string& changelogUrlOut) +bool CemuUpdateWindow::QueryUpdateInfo(std::string& downloadUrlOut, std::string& changelogUrlOut, std::array& fileSha256Out) { + std::string rdString = GenerateSecureRandomString(); + if (rdString.empty()) + return false; std::string buffer; std::string urlStr("https://cemu.info/api2/version.php?v="); auto* curl = curl_easy_init(); - urlStr.append(_curlUrlEscape(curl, BUILD_VERSION_STRING)); -#if BOOST_OS_LINUX || BOOST_OS_BSD // dummy placeholder on BSD for now - urlStr.append("&platform=linux_appimage"); -#elif BOOST_OS_WINDOWS - urlStr.append("&platform=windows"); -#elif BOOST_OS_MACOS - urlStr.append("&platform=macos_bundle"); -#else -#error Name for current platform is missing -#endif -#if defined(__aarch64__) - urlStr.append("_aarch64"); -#elif defined(ARCH_X86_64) - urlStr.append("_x86_64"); -#else - urlStr.append("_unknown"); -#endif -#if BOOST_OS_BSD - return false; // BSD users must update from source; no binary available -#endif + urlStr.append(CurlUrlEscape(curl, BUILD_VERSION_STRING)); + + std::string platformIdentifier = GetPlatformUpdateIdentifier(); + if (platformIdentifier.empty()) + return false; + + urlStr.append("&platform="); + urlStr.append(platformIdentifier); const auto& config = GetWxGUIConfig(); + urlStr.append("&s="); + urlStr.append(rdString); if(config.receive_untested_updates) urlStr.append("&allowNewUpdates=1"); @@ -138,8 +216,9 @@ bool CemuUpdateWindow::QueryUpdateInfo(std::string& downloadUrlOut, std::string& curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteStringCallback); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); bool result = false; CURLcode cr = curl_easy_perform(curl); @@ -159,16 +238,32 @@ bool CemuUpdateWindow::QueryUpdateInfo(std::string& downloadUrlOut, std::string& for (const auto& token : boost::tokenizer(buffer, sep)) tokens.emplace_back(token); - if (tokens.size() >= 3 && tokens[0] == "UPDATE") + if (tokens.size() >= 5 && tokens[0] == "UPDATE") { // first token: "UPDATE" // second token: Download URL // third token: Changelog URL + // fourth token: SHA256 + // fifth token: Signature // we allow more tokens in case we ever want to add extra information for future releases - downloadUrlOut = _curlUrlUnescape(curl, tokens[1]); - changelogUrlOut = _curlUrlUnescape(curl, tokens[2]); + downloadUrlOut = CurlUrlUnescape(curl, tokens[1]); + changelogUrlOut = CurlUrlUnescape(curl, tokens[2]); if (!downloadUrlOut.empty() && !changelogUrlOut.empty()) result = true; + // check signature + std::string signedMessage = platformIdentifier.append("|").append(tokens[0]).append("|").append(tokens[1]).append("|").append(tokens[2]).append("|").append(tokens[3]).append("|").append(rdString); + bool isValidSignature = VerifyUpdateResponseSignature(signedMessage, tokens[4]); + if (!isValidSignature) + { + cemuLog_log(LogType::Force, "Failed server signature check"); + return false; + } + std::vector fileShaVec; + if (!HexToBytes(tokens[3], fileShaVec)) + return false; + if (fileShaVec.size() != 32) + return false; + std::memcpy(fileSha256Out.data(), fileShaVec.data(), 32); } } else @@ -176,7 +271,6 @@ bool CemuUpdateWindow::QueryUpdateInfo(std::string& downloadUrlOut, std::string& cemuLog_log(LogType::Force, "Update check failed with CURL error {}", (int)cr); cemu_assert_debug(false); } - curl_easy_cleanup(curl); return result; } @@ -189,12 +283,12 @@ std::future CemuUpdateWindow::IsUpdateAvailableAsync() bool CemuUpdateWindow::CheckVersion() { std::string downloadUrl, changelogUrl; - return QueryUpdateInfo(downloadUrl, changelogUrl); + std::array fileSha256; + return QueryUpdateInfo(downloadUrl, changelogUrl, fileSha256); } -int CemuUpdateWindow::ProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, - curl_off_t ulnow) +int CemuUpdateWindow::ProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { auto* thisptr = (CemuUpdateWindow*)clientp; auto* event = new wxCommandEvent(wxEVT_PROGRESS); @@ -203,179 +297,217 @@ int CemuUpdateWindow::ProgressCallback(void* clientp, curl_off_t dltotal, curl_o return 0; } -bool CemuUpdateWindow::DownloadCemuZip(const std::string& url, const fs::path& filename) +void CemuUpdateWindow::SubmitWorkerResult(Result newWorkerState) { - FileStream* fsUpdateFile = FileStream::createFile2(filename); - if (!fsUpdateFile) - return false; + auto* event = new wxCommandEvent(wxEVT_RESULT); + event->SetInt((int)newWorkerState); + wxQueueEvent(this, event); +} - bool result = false; +static std::atomic s_cemuUpdateCancelRequest{false}; + +bool CemuUpdateWindow::DownloadCemuUpdateFile(const std::string& url, const fs::path& filename) +{ + if (!fs::exists(filename.parent_path())) + fs::create_directories(filename.parent_path()); auto* curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_NOBODY, 1); curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); auto r = curl_easy_perform(curl); - if (r == CURLE_OK) + if (r != CURLE_OK) { - long http_code = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - if (http_code != 0 && http_code != 200) - { - cemuLog_log(LogType::Force, "Unable to download cemu update zip file from {} (http error: {})", url, http_code); - curl_easy_cleanup(curl); - return false; - } - - curl_off_t update_size; - if (curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &update_size) == CURLE_OK) - m_gaugeMaxValue = (int)update_size; - - - auto _curlWriteData = +[](void* ptr, size_t size, size_t nmemb, void* ctx) -> size_t - { - FileStream* fs = (FileStream*)ctx; - const size_t writeSize = size * nmemb; - fs->writeData(ptr, writeSize); - return writeSize; - }; - - curl_easy_setopt(curl, CURLOPT_NOBODY, 0); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _curlWriteData); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, fsUpdateFile); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); - curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallback); - curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this); - - auto curl_result = std::async(std::launch::async, [](CURL* curl, long* http_code) - { - const auto r = curl_easy_perform(curl); - curl_easy_cleanup(curl); - return r; - }, curl, &http_code); - while (!curl_result.valid()) - { - if (m_order == WorkerOrder::Exit) - return false; - - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - - result = curl_result.get() == CURLE_OK; - - delete fsUpdateFile; - } - else - { - cemuLog_log(LogType::Force, "Cemu zip download failed with error {}", r); + cemuLog_log(LogType::Force, "Cemu update download failed with error {}", r); curl_easy_cleanup(curl); + return false; } - - if (!result && fs::exists(filename)) + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + if (http_code != 0 && http_code != 200) { - try - { - fs::remove(filename); - } - catch (const std::exception& ex) - { - cemuLog_log(LogType::Force, "can't remove update.zip on error: {}", ex.what()); - } - } - return result; -} - -bool CemuUpdateWindow::ExtractUpdate(const fs::path& zipname, const fs::path& targetpath, std::string& cemuFolderName) -{ - cemuFolderName.clear(); - // open downloaded zip - int err; - auto* za = zip_open(zipname.string().c_str(), ZIP_RDONLY, &err); - if (za == nullptr) - { - cemuLog_log(LogType::Force, "Cannot open zip file: {}", zipname.string()); + cemuLog_log(LogType::Force, "Unable to download cemu update file from {} (http error: {})", url, http_code); + curl_easy_cleanup(curl); return false; } - const auto count = zip_get_num_entries(za, 0); - m_gaugeMaxValue = count; - for (auto i = 0; i < count; i++) + curl_off_t update_size; + if (curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &update_size) == CURLE_OK) + m_gaugeMaxValue = (int)update_size; + + std::vector fileBuffer; + + s_cemuUpdateCancelRequest = false; + auto _curlWriteData = +[](void* ptr, size_t size, size_t nmemb, void* ctx) -> size_t { - if (m_order == WorkerOrder::Exit) - return false; + if (s_cemuUpdateCancelRequest) + return 0; + std::vector* fileBufferVec = (std::vector*)ctx; + const size_t writeSize = size * nmemb; + fileBufferVec->insert(fileBufferVec->end(), (uint8*)ptr, (uint8*)ptr + writeSize); + return writeSize; + }; - zip_stat_t sb{}; - if (zip_stat_index(za, i, 0, &sb) == 0) + curl_easy_setopt(curl, CURLOPT_NOBODY, 0); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _curlWriteData); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &fileBuffer); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallback); + curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this); + + auto curl_result = std::async(std::launch::async, [](CURL* curl, long* http_code) { - fs::path fname = targetpath; - fname /= sb.name; + const auto r = curl_easy_perform(curl); + return r; + }, curl, &http_code); + while (true) + { + auto r = curl_result.wait_for(std::chrono::milliseconds(100)); + if (r == std::future_status::ready) + break; + if (m_order == WorkerOrder::Exit && !s_cemuUpdateCancelRequest) + s_cemuUpdateCancelRequest = true; + } + curl_easy_cleanup(curl); + CURLcode curlResult = curl_result.get(); + if (curlResult == CURLE_ABORTED_BY_CALLBACK) + { + return false; + } + else if (curlResult != CURLE_OK) + { + return false; + } + // store file to temp location + std::error_code ec; + fs::remove(filename, ec); + if (!FileStream::WriteFileAtomic(filename, fileBuffer)) + { + cemuLog_log(LogType::Force, "Failed to store update file"); + return false; + } + // read back file and verify the hash + auto diskFileData = FileStream::LoadIntoMemory(filename); + if (!diskFileData) + { + cemuLog_log(LogType::Force, "Failed to read back update file"); + return false; + } + // verify the file hash + std::array hash; + EVP_Q_digest(nullptr, "SHA256", nullptr, diskFileData->data(), diskFileData->size(), hash.data(), nullptr); + if (hash != m_fileSHA256) + { + cemuLog_log(LogType::Force, "File hash of downloaded update does not match"); + fs::remove(filename, ec); + return false; + } + SubmitWorkerResult(Result::UpdateDownloaded); + return true; +} - const auto len = strlen(sb.name); - if (strcmp(sb.name, ".") == 0 || strcmp(sb.name, "..") == 0) +bool CemuUpdateWindow::ExtractZipUpdate(const fs::path& zipname, const fs::path& targetpath) +{ + ZipArchive archive(zipname); + if (!archive.IsValid()) + { + cemuLog_log(LogType::Force, "Cannot open zip file: {}", _pathToUtf8(zipname)); + return false; + } + const size_t count = archive.GetNumEntries(); + m_gaugeMaxValue = count; + // the update files are in a directory called "Cemu_". Find the name of the directory + std::string cemuPathPrefix; + for (size_t i=0; i fileData; + if (!archive.ExtractIntoMemoryByIndex(i, fileData)) + { + cemuLog_log(LogType::Force, "Failed to extract file {}", entry.fullPath); + return false; + } + // make sure the folder exists + const fs::path fname = targetpath / entryPath; + try + { + fs::create_directories(fname.parent_path()); + } + catch (const std::exception& ex) + { + SystemException sys(ex); + cemuLog_log(LogType::Force, "can't create folder for update file \"{}\": {}", entry.fullPath, sys.what()); + return false; + } + } + } + // check if there is enough disk space (to keep this simple we assume 100MB, generally releases should be below that) + std::error_code ec; + const fs::space_info targetSpace = fs::space(ActiveSettings::GetExecutablePath().parent_path(), ec); + if (targetSpace.free <= 100 * 1024 * 1024) + { + cemuLog_log(LogType::Force, "Not enough disk space available for update"); + return false; + } + // actually extract and replace files + for (size_t i = 0; i < count; i++) + { + ZipArchive::ZipFileEntry entry; + if (archive.GetFileEntryByIndex(i, entry)) + { + if (!entry.fullPath.starts_with(cemuPathPrefxWithSlash)) continue; - } - if (sb.name[len - 1] == '/' || sb.name[len - 1] == '\\') + const fs::path entryPath = _utf8ToPath(entry.fullPath.substr(cemuPathPrefxWithSlash.length())); + // read into memory + std::vector fileData; + if (!archive.ExtractIntoMemoryByIndex(i, fileData)) { - // directory - try - { - if (!exists(fname)) - create_directory(fname); - } - catch (const std::exception& ex) - { - SystemException sys(ex); - cemuLog_log(LogType::Force, "can't create folder \"{}\" for update: {}", sb.name, sys.what()); - } - // the root should have only one Cemu_... directory, we track it here - if ((std::count(sb.name, sb.name + len, '/') + std::count(sb.name, sb.name + len, '\\')) == 1) - { - if (!cemuFolderName.empty()) - cemuLog_log(LogType::Force, "update zip has multiple folders in root"); - cemuFolderName.assign(sb.name, len - 1); - } - continue; + cemu_assert(false); // extraction tested earlier, it should not fail here } - - // file - auto* zf = zip_fopen_index(za, i, 0); - if (!zf) + const fs::path fname = targetpath / entryPath; + if ( !FileStream::WriteFileAtomic(fname, fileData, true) ) { - cemuLog_log(LogType::Force, "can't open zip file \"{}\"", sb.name); - zip_close(za); - return false; + cemuLog_log(LogType::Force, "Failed to update {}", _pathToUtf8(fname)); + // continue regardless } - - std::vector buffer(sb.size); - const auto read = zip_fread(zf, buffer.data(), sb.size); - if (read != (sint64)sb.size) - { - cemuLog_log(LogType::Force, "could only read 0x{:x} of 0x{:x} bytes from zip file \"{}\"", read, sb.size, sb.name); - zip_close(za); - return false; - } - - auto* file = fopen(fname.string().c_str(), "wb"); - if (file == nullptr) - { - cemuLog_log(LogType::Force, "can't create update file \"{}\"", sb.name); - zip_close(za); - return false; - } - - fwrite(buffer.data(), 1, buffer.size(), file); - fflush(file); - fclose(file); - - zip_fclose(zf); - if ((i / 10) * 10 == i) { auto* event = new wxCommandEvent(wxEVT_PROGRESS); - event->SetInt(i); + event->SetInt(static_cast(i)); wxQueueEvent(this, event); } } @@ -385,176 +517,244 @@ bool CemuUpdateWindow::ExtractUpdate(const fs::path& zipname, const fs::path& ta event->SetInt(m_gaugeMaxValue); wxQueueEvent(this, event); - zip_close(za); - return true; } -void CemuUpdateWindow::WorkerThread() +#if BOOST_OS_WINDOWS +bool CemuUpdateWindow::WorkerThread_Windows() { const auto tmppath = fs::temp_directory_path() / L"cemu_update"; std::error_code ec; - // clean leftovers if (exists(tmppath)) remove_all(tmppath, ec); + const auto updateSrcFile = tmppath / L"update.zip"; + if (!DownloadCemuUpdateFile(m_downloadUrl, updateSrcFile)) + return false; + if (m_order == WorkerOrder::Exit) + return false; + // extract and replace files + if (!ExtractZipUpdate(updateSrcFile, ActiveSettings::GetExecutablePath().parent_path())) + { + cemuLog_log(LogType::Force, "Extracting Cemu zip failed"); + return false; + } + SubmitWorkerResult(Result::ExtractSuccess); + // set relaunch path + fs::path newExePath = ActiveSettings::GetExecutablePath(); + newExePath = newExePath.parent_path().append("Cemu.exe"); + m_restartFile = newExePath; + return true; +} +#endif + +#if BOOST_OS_LINUX +bool CemuUpdateWindow::WorkerThread_AppImage() +{ + const auto tmppath = fs::temp_directory_path() / L"cemu_update"; + std::error_code ec; + if (exists(tmppath, ec)) + remove_all(tmppath, ec); + const auto updateSrcFile = tmppath / L"Cemu.AppImage"; + if (!DownloadCemuUpdateFile(m_downloadUrl, updateSrcFile)) + return false; + if (m_order == WorkerOrder::Exit) + return false; + const char* appimage_path = std::getenv("APPIMAGE"); + auto backupExecutable = fs::path(appimage_path); + backupExecutable.replace_extension( _utf8ToPath(_pathToUtf8(backupExecutable.extension()).append(".backup"))); + const char* filePath = updateSrcFile.c_str(); + mode_t permissions = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; + fs::rename(appimage_path, backupExecutable, ec); + if (ec) + { + cemuLog_log(LogType::Force, "Failed to rename current .AppImage for update replacement"); + return false; + } + m_restartFile = appimage_path; + chmod(filePath, permissions); + wxString wxAppPath = wxString::FromUTF8(appimage_path); + wxCopyFile (wxString::FromUTF8(_pathToUtf8(updateSrcFile)), wxAppPath); + return true; +} +#endif + +#if BOOST_OS_MACOS +bool CemuUpdateWindow::WorkerThread_MacBundle() +{ + const fs::path tempPath = fs::temp_directory_path() / "cemu_update"; + + std::error_code ec; + fs::remove_all(tempPath, ec); + fs::create_directories(tempPath, ec); + if (ec) + return false; + + const fs::path dmgPath = tempPath / "cemu.dmg"; + if (!DownloadCemuUpdateFile(m_downloadUrl, dmgPath)) + return false; + + if (m_order == WorkerOrder::Exit) + return false; + + // .../Cemu.app/Contents/MacOS/Cemu + const fs::path executable = ActiveSettings::GetExecutablePath(); + const fs::path appBundle = executable.parent_path().parent_path().parent_path(); + if (appBundle.extension() != ".app") + { + cemuLog_log(LogType::Force, "Cemu is not running from a macOS app bundle"); + return false; + } + + const fs::path scriptPath = tempPath / "update.sh"; + const fs::path mountPath = tempPath / "mount"; + + auto ShellQuote = [](std::string_view value) + { + std::string result = "'"; + for (const char c : value) + { + if (c == '\'') + result += "'\\''"; + else + result += c; + } + result += "'"; + return result; + }; + + std::string dmg = ShellQuote(_pathToUtf8(dmgPath)); + std::string app = ShellQuote(_pathToUtf8(appBundle)); + std::string mount = ShellQuote(_pathToUtf8(mountPath)); + + std::string script; + + script += "#!/bin/sh\n"; + script += "DMG_PATH=" + ShellQuote(_pathToUtf8(dmgPath)) + "\n"; + script += "APP_BUNDLE=" + ShellQuote(_pathToUtf8(appBundle)) + "\n"; + script += "MOUNT_PATH=" + ShellQuote(_pathToUtf8(mountPath)) + "\n"; + + script += R"SH( +set -eu + +STAGED_BUNDLE="${APP_BUNDLE}.update" +BACKUP_BUNDLE="${APP_BUNDLE}.backup" +MOUNTED=0 + +finish() +{ + STATUS=$? + trap - EXIT HUP INT TERM + + if [ "$MOUNTED" -eq 1 ]; then + /usr/bin/hdiutil detach "$MOUNT_PATH" >/dev/null 2>&1 || true + fi + + if [ -d "$APP_BUNDLE" ]; then + /usr/bin/open -n "$APP_BUNDLE" || true + elif [ -x "$BACKUP_BUNDLE/Contents/MacOS/Cemu" ]; then + "$BACKUP_BUNDLE/Contents/MacOS/Cemu" & + fi + + exit "$STATUS" +} + +trap finish EXIT HUP INT TERM + +/bin/rm -rf "$MOUNT_PATH" "$STAGED_BUNDLE" +/bin/mkdir -p "$MOUNT_PATH" + +/usr/bin/hdiutil attach \ + -nobrowse \ + -readonly \ + -mountpoint "$MOUNT_PATH" \ + "$DMG_PATH" + +MOUNTED=1 + +/usr/bin/ditto \ + "$MOUNT_PATH/Cemu.app" \ + "$STAGED_BUNDLE" + +/usr/bin/codesign \ + --verify \ + --strict \ + "$STAGED_BUNDLE/Contents/MacOS/Cemu" + +/usr/bin/hdiutil detach "$MOUNT_PATH" +MOUNTED=0 + +/bin/rm -rf "$BACKUP_BUNDLE" +/bin/mv "$APP_BUNDLE" "$BACKUP_BUNDLE" + +if ! /bin/mv "$STAGED_BUNDLE" "$APP_BUNDLE"; then + /bin/mv "$BACKUP_BUNDLE" "$APP_BUNDLE" || true + exit 1 +fi +)SH"; + + std::vector scriptData(script.begin(), script.end()); + if (!FileStream::WriteFileAtomic(scriptPath, scriptData)) + { + cemuLog_log(LogType::Force, "Failed to create macOS updater script"); + return false; + } + if (chmod(scriptPath.c_str(), S_IRUSR | S_IWUSR | S_IXUSR) != 0) + { + cemuLog_log(LogType::Force, "Failed to make updater script executable"); + return false; + } + // run the script on exit + m_restartFile = scriptPath; + return true; +} +#endif + +void CemuUpdateWindow::WorkerThread() +{ + // start writing log.txt if not created yet + cemuLog_createLogFile(false); while (true) { std::unique_lock lock(m_mutex); - while (m_order == WorkerOrder::Idle) - m_condition.wait_for(lock, std::chrono::milliseconds(125)); - - if (m_order == WorkerOrder::Exit) - break; - - try + WorkerOrder nextWorkOrder = WorkerOrder::Idle; + while (true) { - if (m_order == WorkerOrder::CheckVersion) - { - auto* event = new wxCommandEvent(wxEVT_RESULT); - if (QueryUpdateInfo(m_downloadUrl, m_changelogUrl)) - event->SetInt((int)Result::UpdateAvailable); - else - event->SetInt((int)Result::NoUpdateAvailable); - - wxQueueEvent(this, event); - } - else if (m_order == WorkerOrder::UpdateVersion) - { - // download update - const std::string url = m_downloadUrl; - if (!exists(tmppath)) - create_directory(tmppath); + nextWorkOrder = m_order.exchange(WorkerOrder::Idle); + if (nextWorkOrder != WorkerOrder::Idle) + break; + m_condition.wait_for(lock, std::chrono::milliseconds(125)); + } + if (nextWorkOrder == WorkerOrder::Exit) + break; + if (nextWorkOrder == WorkerOrder::CheckVersion) + { + auto* event = new wxCommandEvent(wxEVT_RESULT); + if (QueryUpdateInfo(m_downloadUrl, m_changelogUrl, m_fileSHA256)) + event->SetInt((int)Result::UpdateAvailable); + else + event->SetInt((int)Result::NoUpdateAvailable); + wxQueueEvent(this, event); + } + else if (nextWorkOrder == WorkerOrder::UpdateVersion) + { + // download update + const std::string url = m_downloadUrl; + bool r = false; #if BOOST_OS_WINDOWS - const auto update_file = tmppath / L"update.zip"; -#elif BOOST_OS_LINUX || BOOST_OS_BSD // dummy placeholder on BSD for now - const auto update_file = tmppath / L"Cemu.AppImage"; -#elif BOOST_OS_MACOS - const auto update_file = tmppath / L"cemu.dmg"; -#endif - if (DownloadCemuZip(url, update_file)) - { - auto* event = new wxCommandEvent(wxEVT_RESULT); - event->SetInt((int)Result::UpdateDownloaded); - wxQueueEvent(this, event); - } - else - { - auto* event = new wxCommandEvent(wxEVT_RESULT); - event->SetInt((int)Result::UpdateDownloadError); - wxQueueEvent(this, event); - m_order = WorkerOrder::Idle; - continue; - } - if (m_order == WorkerOrder::Exit) - break; - - // extract - std::string cemuFolderName; -#if BOOST_OS_WINDOWS - if (!ExtractUpdate(update_file, tmppath, cemuFolderName)) - { - cemuLog_log(LogType::Force, "Extracting Cemu zip failed"); - break; - } - if (cemuFolderName.empty()) - { - cemuLog_log(LogType::Force, "Cemu folder not found in zip"); - break; - } -#endif - const auto expected_path = tmppath / cemuFolderName; - if (exists(expected_path)) - { - auto* event = new wxCommandEvent(wxEVT_RESULT); - event->SetInt((int)Result::ExtractSuccess); - wxQueueEvent(this, event); - } - else - { - auto* event = new wxCommandEvent(wxEVT_RESULT); - event->SetInt((int)Result::ExtractError); - wxQueueEvent(this, event); - - if (exists(tmppath)) - { - try - { - fs::remove(tmppath); - } - catch (const std::exception& ex) - { - SystemException sys(ex); - cemuLog_log(LogType::Force, "can't remove extracted tmp files: {}", sys.what()); - } - } - - continue; - } - - if (m_order == WorkerOrder::Exit) - break; - - // apply update - fs::path exePath = ActiveSettings::GetExecutablePath(); -#if BOOST_OS_WINDOWS - std::wstring target_directory = exePath.parent_path().generic_wstring(); - if (target_directory[target_directory.size() - 1] == '/') - target_directory = target_directory.substr(0, target_directory.size() - 1); // remove trailing / - - // get exe name - const auto exec = ActiveSettings::GetExecutablePath(); - const auto target_exe = fs::path(exec).replace_extension("exe.backup"); - fs::rename(exec, target_exe); - m_restartFile = exec; + r = WorkerThread_Windows(); #elif BOOST_OS_LINUX - const char* appimage_path = std::getenv("APPIMAGE"); - const auto target_exe = fs::path(appimage_path).replace_extension("AppImage.backup"); - const char* filePath = update_file.c_str(); - mode_t permissions = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; - fs::rename(appimage_path, target_exe); - m_restartFile = appimage_path; - chmod(filePath, permissions); - wxString wxAppPath = wxString::FromUTF8(appimage_path); - wxCopyFile (wxT("/tmp/cemu_update/Cemu.AppImage"), wxAppPath); -#endif -#if BOOST_OS_WINDOWS - const auto index = expected_path.wstring().size(); - int counter = 0; - for (const auto& it : fs::recursive_directory_iterator(expected_path)) - { - const auto filename = it.path().wstring().substr(index); - auto target_file = target_directory + filename; - try - { - if (is_directory(it)) - { - if (!fs::exists(target_file)) - fs::create_directory(target_file); - } - else - { - if (it.path().filename() == L"Cemu.exe") - fs::rename(it.path(), fs::path(target_file).replace_filename(exec.filename())); - else - fs::rename(it.path(), target_file); - } - } - catch (const std::exception& ex) - { - SystemException sys(ex); - cemuLog_log(LogType::Force, "applying update error: {}", sys.what()); - } - - if ((counter++ % 10) == 0) - { - auto* event = new wxCommandEvent(wxEVT_PROGRESS); - event->SetInt(counter); - wxQueueEvent(this, event); - } - } + r = WorkerThread_AppImage(); +#elif BOOST_OS_BSD + // dummy placeholder on BSD for now +#elif BOOST_OS_MACOS + r = WorkerThread_MacBundle(); #endif + // update done + if (r) + { + // report success auto* event = new wxCommandEvent(wxEVT_PROGRESS); event->SetInt(m_gaugeMaxValue); wxQueueEvent(this, event); @@ -563,66 +763,21 @@ void CemuUpdateWindow::WorkerThread() result_event->SetInt((int)Result::Success); wxQueueEvent(this, result_event); } + else if (m_order != WorkerOrder::Exit) + { + SubmitWorkerResult(Result::UpdateDownloadError); + } } - catch (const std::exception& ex) - { - SystemException sys(ex); - cemuLog_log(LogType::Force, "update error: {}", sys.what()); - - // clean leftovers - if (exists(tmppath)) - remove_all(tmppath, ec); - - auto* result_event = new wxCommandEvent(wxEVT_RESULT); - result_event->SetInt((int)Result::Error); - wxQueueEvent(this, result_event); - } - - m_order = WorkerOrder::Idle; } } void CemuUpdateWindow::OnClose(wxCloseEvent& event) { event.Skip(); - -#if BOOST_OS_WINDOWS - if (m_restartRequired && !m_restartFile.empty() && fs::exists(m_restartFile)) - { - PROCESS_INFORMATION pi{}; - STARTUPINFOW si{}; - si.cb = sizeof(si); - - std::wstring cmdline = GetCommandLineW(); - const auto index = cmdline.find('"', 1); - cemu_assert_debug(index != std::wstring::npos); - cmdline = L"\"" + boost::nowide::widen(_pathToUtf8(m_restartFile)) + L"\"" + cmdline.substr(index + 1); - - HANDLE lock = CreateMutexW(nullptr, TRUE, L"Global\\cemu_update_lock"); - CreateProcessW(nullptr, (wchar_t*)cmdline.c_str(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi); - - exit(0); - } -#elif BOOST_OS_LINUX - if (m_restartRequired && !m_restartFile.empty() && fs::exists(m_restartFile)) - { - const char* appimage_path = std::getenv("APPIMAGE"); - execlp(appimage_path, appimage_path, (char *)NULL); - - exit(0); - } -#elif BOOST_OS_MACOS if (m_restartRequired) { - const auto tmppath = fs::temp_directory_path() / L"cemu_update/Cemu.dmg"; - fs::path exePath = ActiveSettings::GetExecutablePath().parent_path(); - const auto appResources = exePath.parent_path().parent_path() / L"Resources"; - const auto apppath = appResources / L"update.sh"; - execlp("sh", "sh", apppath.c_str(), NULL); - - exit(0); - } -#endif + wxGetApp().RequestRestart(m_restartFile); + } } @@ -657,6 +812,7 @@ void CemuUpdateWindow::OnResult(wxCommandEvent& event) break; case Result::UpdateDownloadError: m_updateButton->Enable(); + m_cancelButton->Enable(); m_text->SetLabel(_("Couldn't download the update!")); break; case Result::ExtractSuccess: diff --git a/src/gui/wxgui/CemuUpdateWindow.h b/src/gui/wxgui/CemuUpdateWindow.h index e3836da3..4ab8c269 100644 --- a/src/gui/wxgui/CemuUpdateWindow.h +++ b/src/gui/wxgui/CemuUpdateWindow.h @@ -30,16 +30,17 @@ private: void OnGaugeUpdate(wxCommandEvent& event); static size_t WriteStringCallback(char* ptr, size_t size, size_t nmemb, void* userdata); - static bool QueryUpdateInfo(std::string& downloadUrlOut, std::string& changelogUrlOut); + static bool QueryUpdateInfo(std::string& downloadUrlOut, std::string& changelogUrlOut, std::array& fileSha256Out); static bool CheckVersion(); static int ProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow); - bool DownloadCemuZip(const std::string& url, const fs::path& filename); - bool ExtractUpdate(const fs::path& zipname, const fs::path& targetpath, std::string& cemuFolderName); + bool DownloadCemuUpdateFile(const std::string& url, const fs::path& filename); + bool ExtractZipUpdate(const fs::path& zipname, const fs::path& targetpath); enum class WorkerOrder { Idle, + InProgress, // work command received Exit, CheckVersion, UpdateVersion, @@ -57,10 +58,24 @@ private: }; std::mutex m_mutex; std::condition_variable m_condition; - WorkerOrder m_order = WorkerOrder::CheckVersion; + std::atomic m_order = WorkerOrder::CheckVersion; void WorkerThread(); + void SubmitWorkerResult(Result newWorkerState); + +#if BOOST_OS_WINDOWS + bool WorkerThread_Windows(); +#endif +#if BOOST_OS_LINUX + bool WorkerThread_AppImage(); +#endif +#if BOOST_OS_MACOS + bool WorkerThread_MacBundle(); +#endif + + std::string m_downloadUrl, m_changelogUrl; + std::array m_fileSHA256; int m_gaugeMaxValue = 0; std::thread m_thread; diff --git a/src/gui/wxgui/ChecksumTool.cpp b/src/gui/wxgui/ChecksumTool.cpp index f9cf3c90..8f9698a6 100644 --- a/src/gui/wxgui/ChecksumTool.cpp +++ b/src/gui/wxgui/ChecksumTool.cpp @@ -200,15 +200,16 @@ void ChecksumTool::LoadOnlineData() const // check latest version /* - https://api.github.com/repos/teamcemu/title-checksums/branches/master - https://api.github.com/repos/teamcemu/title-checksums/commits?per_page=1 + https://api.github.com/repos/cemu-project/title-checksums/branches/master + https://api.github.com/repos/cemu-project/title-checksums/commits?per_page=1 */ std::string data; auto* curl = curl_easy_init(); - curl_easy_setopt(curl, CURLOPT_URL, "https://api.github.com/repos/teamcemu/title-checksums/commits/master"); + curl_easy_setopt(curl, CURLOPT_URL, "https://api.github.com/repos/cemu-project/title-checksums/commits/master"); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1); + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING); curl_easy_perform(curl); @@ -240,10 +241,11 @@ void ChecksumTool::LoadOnlineData() const { std::string data; auto* curl = curl_easy_init(); - curl_easy_setopt(curl, CURLOPT_URL, "https://github.com/TeamCemu/title-checksums/archive/master.zip"); + curl_easy_setopt(curl, CURLOPT_URL, "https://github.com/cemu-project/title-checksums/archive/master.zip"); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1); + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING); @@ -258,7 +260,7 @@ void ChecksumTool::LoadOnlineData() const zip_error_t error; zip_error_init(&error); zip_source_t* src; - if ((src = zip_source_buffer_create(data.data(), data.size(), 1, &error)) == nullptr) + if ((src = zip_source_buffer_create(data.data(), data.size(), 0, &error)) == nullptr) { zip_error_fini(&error); return; @@ -267,6 +269,8 @@ void ChecksumTool::LoadOnlineData() const auto* za = zip_open_from_source(src, ZIP_RDONLY, &error); if (!za) { + zip_source_free(src); + zip_error_fini(&error); wxQueueEvent(m_verify_online, new wxCommandEvent(wxEVT_ENABLE)); return; } @@ -324,6 +328,9 @@ void ChecksumTool::LoadOnlineData() const zip_fclose(zipFile); } + zip_discard(za); + zip_error_fini(&error); + std::ofstream file(checksum_path / "commit.txt"); if (file.is_open()) { diff --git a/src/gui/wxgui/DownloadGraphicPacksWindow.cpp b/src/gui/wxgui/DownloadGraphicPacksWindow.cpp index 08e98fc0..b2f8b849 100644 --- a/src/gui/wxgui/DownloadGraphicPacksWindow.cpp +++ b/src/gui/wxgui/DownloadGraphicPacksWindow.cpp @@ -51,8 +51,9 @@ bool DownloadGraphicPacksWindow::curlDownloadFile(const char *url, curlDownloadF curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, downloadState); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1); + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING); downloadState->fileData.resize(0); @@ -120,12 +121,12 @@ void DownloadGraphicPacksWindow::UpdateThread() queryUrl.append("&"); sprintf(temp, "t=%u", (uint32)std::chrono::seconds(std::time(NULL)).count()); // add a dynamic part to the url to bypass overly aggressive caching (like some proxies do) queryUrl.append(temp); - if (curlDownloadFile(queryUrl.c_str(), &tempDownloadState) && boost::starts_with((const char*)tempDownloadState.fileData.data(), "http")) + if (curlDownloadFile(queryUrl.c_str(), &tempDownloadState)) { // convert downloaded data to url string githubAPIUrl.assign(tempDownloadState.fileData.cbegin(), tempDownloadState.fileData.cend()); } - else + if (githubAPIUrl.empty()) { // cemu api request failed, use hardcoded github url cemuLog_log(LogType::Force, "Graphic pack update request failed or returned invalid URL. Using default repository URL instead"); @@ -214,7 +215,7 @@ void DownloadGraphicPacksWindow::UpdateThread() // init zip source zip_error_init(&error); - if ((src = zip_source_buffer_create(m_downloadState->fileData.data(), m_downloadState->fileData.size(), 1, &error)) == NULL) + if ((src = zip_source_buffer_create(m_downloadState->fileData.data(), m_downloadState->fileData.size(), 0, &error)) == NULL) { zip_error_fini(&error); m_threadState = ThreadError; @@ -285,6 +286,7 @@ void DownloadGraphicPacksWindow::UpdateThread() zip_fclose(zipFile); } + zip_discard(za); zip_error_fini(&error); createGraphicPackDownloadedVersionFile(assetName); m_threadState = ThreadFinished; diff --git a/src/gui/wxgui/MainWindow.cpp b/src/gui/wxgui/MainWindow.cpp index 9eec7dd3..06e20242 100644 --- a/src/gui/wxgui/MainWindow.cpp +++ b/src/gui/wxgui/MainWindow.cpp @@ -242,7 +242,6 @@ EVT_COMMAND(wxID_ANY, wxEVT_GAMELIST_BEGIN_UPDATE, MainWindow::OnGameListBeginUp EVT_COMMAND(wxID_ANY, wxEVT_GAMELIST_END_UPDATE, MainWindow::OnGameListEndUpdate) EVT_COMMAND(wxID_ANY, wxEVT_ACCOUNTLIST_REFRESH, MainWindow::OnAccountListRefresh) EVT_COMMAND(wxID_ANY, wxEVT_SET_WINDOW_TITLE, MainWindow::OnSetWindowTitle) - EVT_COMMAND(wxID_ANY, wxEVT_REQUEST_RECREATE_CANVAS, MainWindow::OnRequestRecreateCanvas) EVT_COMMAND(wxID_ANY, wxEVT_REQUEST_GAME_EXIT, MainWindow::OnRequestGameExit) diff --git a/src/gui/wxgui/MainWindow.h b/src/gui/wxgui/MainWindow.h index deb656ce..483b67fa 100644 --- a/src/gui/wxgui/MainWindow.h +++ b/src/gui/wxgui/MainWindow.h @@ -160,6 +160,7 @@ private: void CafeRecreateCanvas() override; void CafePPCProcessExit() override; + void OnRequestRestart(wxCommandEvent& event); void OnRequestRecreateCanvas(wxCommandEvent& event); void OnRequestGameExit(wxCommandEvent& event); diff --git a/src/main.cpp b/src/main.cpp index 5372acdd..ab915594 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -198,35 +198,18 @@ void requireConsole() void HandlePostUpdate() { - // finalize update process - // delete update cemu.exe.backup if available - const auto filename = ActiveSettings::GetExecutablePath().replace_extension("exe.backup"); - if (fs::exists(filename)) + auto exeBackupPath = ActiveSettings::GetExecutablePath(); + exeBackupPath.replace_extension( _utf8ToPath(_pathToUtf8(exeBackupPath.extension()).append(".backup"))); + std::error_code ec; + if (!fs::exists(exeBackupPath, ec)) + return; + // try to delete update residue, but give up quickly as to not cause a permanent hang + // it may succeed on next turn + for (sint32 i=0; i<3; i++) { -#if BOOST_OS_WINDOWS - HANDLE lock; - do - { - lock = CreateMutexW(nullptr, TRUE, L"Global\\cemu_update_lock"); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } while (lock == nullptr); - const DWORD wait_result = WaitForSingleObject(lock, 2000); - CloseHandle(lock); - - if (wait_result == WAIT_OBJECT_0) - { - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - std::error_code ec; - fs::remove(filename, ec); - } -#else - while (fs::exists(filename)) - { - std::error_code ec; - fs::remove(filename, ec); - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - } -#endif + if (fs::remove(exeBackupPath, ec)) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); } } diff --git a/src/resource/update.sh b/src/resource/update.sh deleted file mode 100755 index 5ff22160..00000000 --- a/src/resource/update.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -APP=$(cd "$(dirname "0")"/;pwd) -hdiutil attach $TMPDIR/cemu_update/cemu.dmg -cp -rf /Volumes/Cemu/Cemu.app "$APP" -hdiutil detach /Volumes/Cemu/ - -open -n -a "$APP/Cemu.app" diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index a23754ef..be10d40d 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -38,6 +38,7 @@ add_library(CemuUtil helpers/StringParser.h helpers/SystemException.h helpers/TempState.h + helpers/ZipArchive.h highresolutiontimer/HighResolutionTimer.cpp highresolutiontimer/HighResolutionTimer.h ImageWriter/bmp.h diff --git a/src/util/helpers/ZipArchive.h b/src/util/helpers/ZipArchive.h new file mode 100644 index 00000000..88cc0704 --- /dev/null +++ b/src/util/helpers/ZipArchive.h @@ -0,0 +1,138 @@ +#pragma once + +#include + +// more secure and robust wrapper over libzip API. Also follows our utf8 conventions for std::string(_view). Read-only access for now +class ZipArchive +{ +public: + struct ZipFileEntry + { + std::string_view fullPath; + std::string_view filename; + }; + + ZipArchive(const fs::path& path) + { + m_zipPath = _pathToUtf8(path); + int zeError{0}; + m_zip = zip_open(m_zipPath.c_str(), ZIP_RDONLY, &zeError); + if (!m_zip) + cemuLog_log(LogType::Force, "Opening zip \"{}\" failed with error code {}", m_zipPath, zeError); + } + + ~ZipArchive() + { + if (m_zip) + zip_close(m_zip); + } + + bool IsValid() + { + return m_zip; + } + + size_t GetNumEntries() + { + cemu_assert(m_zip); // zip must be valid to call this + zip_int64_t num = zip_get_num_entries(m_zip, 0); + if (num < 0) + return 0; + return static_cast(num); + } + + bool GetFileEntryByIndex(size_t index, ZipFileEntry& entry) + { + zip_stat_t st{}; + if (zip_stat_index(m_zip, (zip_uint64_t)index, 0, &st) != 0) + { + cemuLog_log(LogType::Force, "Zip has corrupted entry at index {} in zip {}", index, m_zipPath); + return false; + } + // we only return entries with a valid name + if (!(st.valid & ZIP_STAT_NAME)) + return false; + size_t nameLen = strlen(st.name); + if (nameLen == 0) + return false; + // reject "." and ".." path components + { + std::string_view path = st.name; + size_t start = 0; + while (start <= path.size()) + { + size_t end = path.find('/', start); + if (end == std::string_view::npos) + end = path.size(); + const auto component = path.substr(start, end - start); + if (component == "." || component == "..") + return false; + if (end == path.size()) + break; + start = end + 1; + } + } + // reject backward slash in path, its technically allowed on Linux and macOS, but for Windows this is a path delimiter + // this also rejects UNC paths like \\server\share\file + if (strchr(st.name, '\\')) + return false; + // reject absolute Unix paths + if (st.name[0] == '/') + return false; + // reject Windows drive paths, e.g. C:/foo + if (nameLen >= 2 && + ((st.name[0] >= 'A' && st.name[0] <= 'Z') || (st.name[0] >= 'a' && st.name[0] <= 'z')) && + st.name[1] == ':') + { + return false; + } + // an entry may also be an explicit directory, indicated by a slash at the end + // we do not return those as files either + bool isDir = st.name[nameLen-1] == '/'; + if (isDir) + return false; + entry = {}; + entry.fullPath = st.name; + // extract filename from full path + auto pos = entry.fullPath.find_last_of('/'); + if (pos != std::string_view::npos) + entry.filename = entry.fullPath.substr(pos+1); + else + entry.filename = entry.fullPath; + return true; + } + + bool ExtractIntoMemoryByIndex(size_t index, std::vector& data) + { + zip_file_t* zf = zip_fopen_index(m_zip, index, 0); + if (!zf) + { + cemuLog_log(LogType::Force, "Failed to open file {} in zip {}", index, m_zipPath); + cemu_assert_suspicious(); + return false; + } + data.clear(); + uint8 tmpBuffer[1024*4]; + while (true) + { + // keep calling zip_fread until end of file is reached + zip_int64_t r = zip_fread(zf, tmpBuffer, 1024*4); + if (r == 0) + break; + if (r < 0) + { + cemuLog_log(LogType::Force, "Read error while reading file at index {} in zip {}", index, m_zipPath); + cemu_assert_suspicious(); + zip_fclose(zf); + return false; + } + data.insert(data.end(), tmpBuffer, tmpBuffer + r); + } + zip_fclose(zf); + return true; + } + +private: + zip_t* m_zip{}; + std::string m_zipPath; +};