UI: Refactor auto-updater and make it more robust

This commit is contained in:
Exzap committed 2026-09-10 13:27:23 +02:00
1 parent 7945f102dc
commit a1cd67ca11
17 files changed
+879 -447

No files matched your search

-10
View File
@@ -142,10 +142,8 @@ if (MACOS_BUNDLE)
endif()
endif()
set(UPDATE_SH_PATH "${CMAKE_SOURCE_DIR}/src/resource/update.sh")
set(APP_BUNDLE_DIR "$<TARGET_BUNDLE_DIR:CemuBin>")
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
+55
View File
@@ -242,3 +242,58 @@ void FileStream::SyncReadWriteSeek(bool nextOpIsWrite)
m_prevOperationWasWrite = nextOpIsWrite;
}
bool FileStream::WriteFileAtomic(const fs::path& path, std::span<uint8> 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;
}
+3
View File
@@ -47,6 +47,9 @@ class FileStream
~FileStream();
FileStream() {};
// atomic helper
static bool WriteFileAtomic(const fs::path& path, std::span<uint8> fileData, bool allowTargetFileRename = false);
private:
void SyncReadWriteSeek(bool nextOpIsWrite);
FileStream(const fs::path& path, bool isOpen, bool isWriteable);
+55
View File
@@ -185,3 +185,58 @@ FileStream::FileStream(HANDLE hFile)
m_hFile = hFile;
m_isValid = true;
}
bool FileStream::WriteFileAtomic(const fs::path& path, std::span<uint8> 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;
}
+3
View File
@@ -45,6 +45,9 @@ class FileStream
~FileStream();
FileStream() = default;
// atomic helper
static bool WriteFileAtomic(const fs::path& path, std::span<uint8> fileData, bool allowTargetFileRename = false);
private:
FileStream(HANDLE hFile);
+29
View File
@@ -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);
});
}
+3
View File
@@ -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<const wxLanguageInfo*> m_availableTranslations;
std::optional<fs::path> m_restartExecutable;
};
wxDECLARE_APP(CemuApp);
File diff suppressed because it is too large. Load diff
+19 -4
View File
@@ -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<uint8, 32>& 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<WorkerOrder> 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<uint8, 32> m_fileSHA256;
int m_gaugeMaxValue = 0;
std::thread m_thread;
+14 -7
View File
@@ -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())
{
+7 -5
View File
@@ -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;
-1
View File
@@ -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)
+1
View File
@@ -160,6 +160,7 @@ private:
void CafeRecreateCanvas() override;
void CafePPCProcessExit() override;
void OnRequestRestart(wxCommandEvent& event);
void OnRequestRecreateCanvas(wxCommandEvent& event);
void OnRequestGameExit(wxCommandEvent& event);
+11 -28
View File
@@ -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));
}
}
-8
View File
@@ -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"
+1
View File
@@ -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
+138
View File
@@ -0,0 +1,138 @@
#pragma once
#include <zip.h>
// 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<size_t>(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<uint8>& 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;
};