#include "gui/CemuUpdateWindow.h" #include "Common/version.h" #include "util/helpers/helpers.h" #include "util/helpers/SystemException.h" #include "config/ActiveSettings.h" #include "Common/FileStream.h" #include #include #include #include #include #include #include wxDECLARE_EVENT(wxEVT_RESULT, wxCommandEvent); wxDEFINE_EVENT(wxEVT_RESULT, wxCommandEvent); wxDECLARE_EVENT(wxEVT_PROGRESS, wxCommandEvent); wxDEFINE_EVENT(wxEVT_PROGRESS, wxCommandEvent); CemuUpdateWindow::CemuUpdateWindow(wxWindow* parent) : wxDialog(parent, wxID_ANY, "Cemu update", wxDefaultPosition, wxDefaultSize, wxCAPTION | wxMINIMIZE_BOX | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX) { auto* sizer = new wxBoxSizer(wxVERTICAL); m_gauge = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxSize(500, 20), wxGA_HORIZONTAL); m_gauge->SetValue(0); sizer->Add(m_gauge, 0, wxALL | wxEXPAND, 5); auto* rows = new wxFlexGridSizer(0, 2, 0, 0); rows->AddGrowableCol(1); m_text = new wxStaticText(this, wxID_ANY, "Checking for latest version..."); rows->Add(m_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); { auto* right_side = new wxBoxSizer(wxHORIZONTAL); m_updateButton = new wxButton(this, wxID_ANY, _("Update")); m_updateButton->Bind(wxEVT_BUTTON, &CemuUpdateWindow::OnUpdateButton, this); right_side->Add(m_updateButton, 0, wxALL, 5); m_cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); m_cancelButton->Bind(wxEVT_BUTTON, &CemuUpdateWindow::OnCancelButton, this); right_side->Add(m_cancelButton, 0, wxALL, 5); rows->Add(right_side, 1, wxALIGN_RIGHT, 5); } m_changelog = new wxHyperlinkCtrl(this, wxID_ANY, _("Changelog"), wxEmptyString); rows->Add(m_changelog, 0, wxLEFT | wxBOTTOM | wxRIGHT | wxEXPAND, 5); sizer->Add(rows, 0, wxALL | wxEXPAND, 5); SetSizerAndFit(sizer); Centre(wxBOTH); Bind(wxEVT_CLOSE_WINDOW, &CemuUpdateWindow::OnClose, this); Bind(wxEVT_RESULT, &CemuUpdateWindow::OnResult, this); Bind(wxEVT_PROGRESS, &CemuUpdateWindow::OnGaugeUpdate, this); m_thread = std::thread(&CemuUpdateWindow::WorkerThread, this); m_updateButton->Hide(); m_changelog->Hide(); } CemuUpdateWindow::~CemuUpdateWindow() { m_order = WorkerOrder::Exit; if (m_thread.joinable()) m_thread.join(); } size_t CemuUpdateWindow::WriteStringCallback(char* ptr, size_t size, size_t nmemb, void* userdata) { ((std::string*)userdata)->append(ptr, size * nmemb); return size * nmemb; }; std::string _curlUrlEscape(CURL* curl, const std::string& input) { char* escapedStr = curl_easy_escape(curl, input.c_str(), input.size()); std::string r(escapedStr); curl_free(escapedStr); return r; } 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); } // returns true if update is available and sets output parameters bool CemuUpdateWindow::QueryUpdateInfo(std::string& downloadUrlOut, std::string& changelogUrlOut) { 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 urlStr.append("&platform=linux"); #elif BOOST_OS_WINDOWS urlStr.append("&platform=windows"); #elif BOOST_OS_MACOS urlStr.append("&platform=macos_x86"); #elif #error Name for current platform is missing #endif curl_easy_setopt(curl, CURLOPT_URL, urlStr.c_str()); 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); bool result = false; CURLcode cr = curl_easy_perform(curl); if (cr == CURLE_OK) { long http_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); if (http_code != 0 && http_code != 200) { forceLog_printf("Update check failed (http code: %d)", http_code); cemu_assert_debug(false); return false; } std::vector tokens; const boost::char_separator sep{ "|" }; for (const auto& token : boost::tokenizer(buffer, sep)) tokens.emplace_back(token); if (tokens.size() >= 3 && tokens[0] == "UPDATE") { // first token: "UPDATE" // second token: Download URL // third token: Changelog URL // 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]); if (!downloadUrlOut.empty() && !changelogUrlOut.empty()) result = true; } } else { forceLog_printf("Update check failed with CURL error %d", (int)cr); cemu_assert_debug(false); } curl_easy_cleanup(curl); return result; } std::future CemuUpdateWindow::IsUpdateAvailableAsync() { return std::async(std::launch::async, CheckVersion); } bool CemuUpdateWindow::CheckVersion() { std::string downloadUrl, changelogUrl; return QueryUpdateInfo(downloadUrl, changelogUrl); } 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); event->SetInt((int)dlnow); wxQueueEvent(thisptr, event); return 0; } bool CemuUpdateWindow::DownloadCemuZip(const std::string& url, const fs::path& filename) { FileStream* fsUpdateFile = FileStream::createFile2(filename); if (!fsUpdateFile) return false; bool result = false; 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_FOLLOWLOCATION, 1); auto r = curl_easy_perform(curl); 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); curl_easy_cleanup(curl); } if (!result && fs::exists(filename)) { try { fs::remove(filename); } catch (const std::exception& ex) { forceLog_printf("can't remove update.zip on error: %s", 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()); return false; } const auto count = zip_get_num_entries(za, 0); m_gaugeMaxValue = count; for (auto i = 0; i < count; i++) { if (m_order == WorkerOrder::Exit) return false; zip_stat_t sb{}; if (zip_stat_index(za, i, 0, &sb) == 0) { fs::path fname = targetpath; fname /= sb.name; const auto len = strlen(sb.name); if (strcmp(sb.name, ".") == 0 || strcmp(sb.name, "..") == 0) { // protection continue; } if (sb.name[len - 1] == '/' || sb.name[len - 1] == '\\') { // directory try { if (!exists(fname)) create_directory(fname); } catch (const std::exception& ex) { SystemException sys(ex); forceLog_printf("can't create folder \"%s\" for update: %s", 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()) forceLog_printf("update zip has multiple folders in root"); cemuFolderName.assign(sb.name, len - 1); } continue; } // file auto* zf = zip_fopen_index(za, i, 0); if (!zf) { forceLog_printf("can't open zip file \"%s\"", sb.name); zip_close(za); return false; } std::vector buffer(sb.size); const auto read = zip_fread(zf, buffer.data(), sb.size); if (read != (sint64)sb.size) { forceLog_printf("could only read 0x%x of 0x%x bytes from zip file \"%s\"", read, sb.size, sb.name); zip_close(za); return false; } auto* file = fopen(fname.string().c_str(), "wb"); if (file == nullptr) { forceLog_printf("can't create update file \"%s\"", 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); wxQueueEvent(this, event); } } } auto* event = new wxCommandEvent(wxEVT_PROGRESS); event->SetInt(m_gaugeMaxValue); wxQueueEvent(this, event); zip_close(za); return true; } void CemuUpdateWindow::WorkerThread() { const auto tmppath = fs::temp_directory_path() / L"cemu_update"; std::error_code ec; // clean leftovers if (exists(tmppath)) remove_all(tmppath, ec); 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 { 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); const auto update_file = tmppath / L"update.zip"; 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 (!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; } 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); forceLog_printf("can't remove extracted tmp files: %s", sys.what()); } } continue; } if (m_order == WorkerOrder::Exit) break; // apply update std::wstring target_directory = ActiveSettings::GetPath().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::GetFullPath(); const auto target_exe = fs::path(exec).replace_extension("exe.backup"); fs::rename(exec, target_exe); m_restartFile = exec; 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); forceLog_printf("applying update error: %s", sys.what()); } if ((counter++ / 10) * 10 == counter) { auto* event = new wxCommandEvent(wxEVT_PROGRESS); event->SetInt(counter); wxQueueEvent(this, event); } } auto* event = new wxCommandEvent(wxEVT_PROGRESS); event->SetInt(m_gaugeMaxValue); wxQueueEvent(this, event); auto* result_event = new wxCommandEvent(wxEVT_RESULT); result_event->SetInt((int)Result::Success); wxQueueEvent(this, result_event); } } catch (const std::exception& ex) { SystemException sys(ex); forceLog_printf("update error: %s", 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{}; STARTUPINFO si{}; si.cb = sizeof(si); std::wstring cmdline = GetCommandLineW(); const auto index = cmdline.find('"', 1); cemu_assert_debug(index != std::wstring::npos); cmdline = L"\"" + m_restartFile.wstring() + L"\"" + cmdline.substr(index + 1); HANDLE lock = CreateMutex(nullptr, TRUE, L"Global\\cemu_update_lock"); CreateProcess(nullptr, (wchar_t*)cmdline.c_str(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi); exit(0); } #else cemuLog_log(LogType::Force, "unimplemented - restart on update"); #endif } void CemuUpdateWindow::OnResult(wxCommandEvent& event) { switch ((Result)event.GetInt()) { case Result::NoUpdateAvailable: m_cancelButton->SetLabel(_("Exit")); m_text->SetLabel(_("No update available!")); m_gauge->SetValue(100); break; case Result::UpdateAvailable: { if (!m_changelogUrl.empty()) { m_changelog->SetURL(m_changelogUrl); m_changelog->Show(); } else m_changelog->Hide(); m_updateButton->Show(); m_text->SetLabel(_("Update available!")); m_cancelButton->SetLabel(_("Exit")); break; } case Result::UpdateDownloaded: m_text->SetLabel(_("Extracting update...")); m_gauge->SetValue(0); break; case Result::UpdateDownloadError: m_updateButton->Enable(); m_text->SetLabel(_("Couldn't download the update!")); break; case Result::ExtractSuccess: m_text->SetLabel(_("Applying update...")); m_gauge->SetValue(0); m_cancelButton->Disable(); break; case Result::ExtractError: m_updateButton->Enable(); m_cancelButton->Enable(); m_text->SetLabel(_("Extracting failed!")); break; case Result::Success: m_cancelButton->Enable(); m_updateButton->Hide(); m_text->SetLabel(_("Success")); m_cancelButton->SetLabel(_("Restart")); m_restartRequired = true; break; default:; } } void CemuUpdateWindow::OnGaugeUpdate(wxCommandEvent& event) { const int total_size = m_gaugeMaxValue > 0 ? m_gaugeMaxValue : 10000000; m_gauge->SetValue((event.GetInt() * 100) / total_size); } void CemuUpdateWindow::OnUpdateButton(const wxCommandEvent& event) { std::unique_lock lock(m_mutex); m_order = WorkerOrder::UpdateVersion; m_condition.notify_all(); m_updateButton->Disable(); m_text->SetLabel(_("Downloading update...")); } void CemuUpdateWindow::OnCancelButton(const wxCommandEvent& event) { Close(); }