mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-06-06 01:13:45 -04:00
[frontend] Built-in auto updater (#3845)
Checks latest release and opens a dialog containing the changelog, and allow the user to select a specific build to download. After downloading, it prompts the user to open it. On Windows, this just opens up the zip in File Explorer. In the future setup files will be available. On macOS this opens up the DMG in Finder so the user can drag it to the Applications folder. Android retains the auto-update functionality from before, but updated to the new scheme. Body/View on Forgejo are not implemented, that should be in a future PR. Additionally, moved some common httplib incantations to `Common::Net`. This will serve as the common network accessor and JSON parser from here on out. TODO: - [x] android :( - [x] Search for builds based on keywords, with weights towards certain builds (e.g. macOS will search for dmg then tar.gz, windows msvc then mingw/exe then zip, etc.) - [x] remove linux leftovers - [x] don't allow asset selection on platforms w/o assets - [x] nightly changelog should be in the real FUTURE IMPLEMENTATION: - [ ] Body/View on Forgejo for Android - [ ] Setup files for Windows (Eden/nightly are separate) -- maybe portable/setup selector? - [ ] Something else I'm forgetting Signed-off-by: crueter <crueter@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3845
This commit is contained in:
parent
77decca678
commit
676b1aabfc
23 changed files with 856 additions and 375 deletions
|
|
@ -268,11 +268,6 @@ if (NOT EXISTS ${PROJECT_BINARY_DIR}/${compat_json})
|
||||||
file(WRITE ${PROJECT_BINARY_DIR}/${compat_json} "")
|
file(WRITE ${PROJECT_BINARY_DIR}/${compat_json} "")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (YUZU_LEGACY)
|
|
||||||
message(WARNING "Making legacy build. Performance may suffer.")
|
|
||||||
add_compile_definitions(YUZU_LEGACY)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if (ARCHITECTURE_arm64 AND (ANDROID OR PLATFORM_LINUX))
|
if (ARCHITECTURE_arm64 AND (ANDROID OR PLATFORM_LINUX))
|
||||||
set(HAS_NCE 1)
|
set(HAS_NCE 1)
|
||||||
add_compile_definitions(HAS_NCE=1)
|
add_compile_definitions(HAS_NCE=1)
|
||||||
|
|
@ -424,10 +419,10 @@ if (zstd_ADDED)
|
||||||
add_library(zstd::libzstd ALIAS libzstd_static)
|
add_library(zstd::libzstd ALIAS libzstd_static)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (NOT YUZU_STATIC_ROOM)
|
# nlohmann
|
||||||
# nlohmann
|
AddJsonPackage(nlohmann)
|
||||||
AddJsonPackage(nlohmann)
|
|
||||||
|
|
||||||
|
if (NOT YUZU_STATIC_ROOM)
|
||||||
# zlib
|
# zlib
|
||||||
AddJsonPackage(zlib)
|
AddJsonPackage(zlib)
|
||||||
|
|
||||||
|
|
@ -485,7 +480,7 @@ endfunction()
|
||||||
# =============================================
|
# =============================================
|
||||||
|
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
foreach(fw Carbon Metal Cocoa IOKit CoreVideo CoreMedia)
|
foreach(fw Carbon Metal Cocoa IOKit CoreVideo CoreMedia Security)
|
||||||
find_library(${fw}_LIBRARY ${fw} REQUIRED)
|
find_library(${fw}_LIBRARY ${fw} REQUIRED)
|
||||||
list(APPEND PLATFORM_LIBRARIES ${${fw}_LIBRARY})
|
list(APPEND PLATFORM_LIBRARIES ${${fw}_LIBRARY})
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,11 @@ endif()
|
||||||
set(GIT_DESC ${BUILD_VERSION})
|
set(GIT_DESC ${BUILD_VERSION})
|
||||||
|
|
||||||
# Generate cpp with Git revision from template
|
# Generate cpp with Git revision from template
|
||||||
# Also if this is a CI build, add the build name (ie: Nightly, Canary) to the scm_rev file as well
|
|
||||||
|
|
||||||
# Auto-updater metadata! Must somewhat mirror GitHub/Forgejo API endpoint
|
# TODO(crueter): Stable releases feed.
|
||||||
|
set(BUILD_AUTO_UPDATE_STABLE_REPO "eden-emu/eden")
|
||||||
|
set(BUILD_AUTO_UPDATE_STABLE_API "git.eden-emu.dev")
|
||||||
|
set(BUILD_AUTO_UPDATE_STABLE_API_PATH "/api/v1/repos/")
|
||||||
|
|
||||||
set(BUILD_AUTO_UPDATE_API_PATH "/latest/release.json")
|
set(BUILD_AUTO_UPDATE_API_PATH "/latest/release.json")
|
||||||
if (NIGHTLY_BUILD)
|
if (NIGHTLY_BUILD)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,16 @@ if (NIGHTLY_BUILD)
|
||||||
add_compile_definitions(NIGHTLY_BUILD)
|
add_compile_definitions(NIGHTLY_BUILD)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (YUZU_LEGACY)
|
||||||
|
message(WARNING "Making legacy build. Performance may suffer.")
|
||||||
|
add_compile_definitions(YUZU_LEGACY)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (GENSHIN_SPOOF)
|
||||||
|
message(WARNING "Making Genshin spoof build")
|
||||||
|
add_compile_definitions(GENSHIN_SPOOF)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Set compilation flags
|
# Set compilation flags
|
||||||
if (MSVC AND NOT CXX_CLANG)
|
if (MSVC AND NOT CXX_CLANG)
|
||||||
set(CMAKE_CONFIGURATION_TYPES Debug Release CACHE STRING "" FORCE)
|
set(CMAKE_CONFIGURATION_TYPES Debug Release CACHE STRING "" FORCE)
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ android {
|
||||||
"-DBUILD_TESTING=OFF",
|
"-DBUILD_TESTING=OFF",
|
||||||
"-DYUZU_TESTS=OFF",
|
"-DYUZU_TESTS=OFF",
|
||||||
"-DDYNARMIC_TESTS=OFF",
|
"-DDYNARMIC_TESTS=OFF",
|
||||||
|
"-DENABLE_UPDATE_CHECKER=ON",
|
||||||
*extraCMakeArgs.toTypedArray()
|
*extraCMakeArgs.toTypedArray()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -192,6 +193,12 @@ android {
|
||||||
manifestPlaceholders += mapOf("appNameBase" to "Eden")
|
manifestPlaceholders += mapOf("appNameBase" to "Eden")
|
||||||
resValue("string", "app_name_suffixed", "Eden")
|
resValue("string", "app_name_suffixed", "Eden")
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
arguments.add("-DGENSHIN_SPOOF=ON")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a")
|
abiFilters += listOf("arm64-v8a")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,18 @@ import org.yuzu.yuzu_emu.applets.web.WebBrowser
|
||||||
* with the native side of the Yuzu code.
|
* with the native side of the Yuzu code.
|
||||||
*/
|
*/
|
||||||
object NativeLibrary {
|
object NativeLibrary {
|
||||||
|
data class UpdateResult(
|
||||||
|
var tag: String = "",
|
||||||
|
var title: String = "",
|
||||||
|
var body: String = "",
|
||||||
|
var url: String = "",
|
||||||
|
var assets: MutableList<String> = mutableListOf()
|
||||||
|
) {
|
||||||
|
fun addAsset(asset: String) {
|
||||||
|
assets.add(asset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
var sEmulationActivity = WeakReference<EmulationActivity?>(null)
|
var sEmulationActivity = WeakReference<EmulationActivity?>(null)
|
||||||
|
|
||||||
|
|
@ -240,17 +252,7 @@ object NativeLibrary {
|
||||||
/**
|
/**
|
||||||
* Checks for available updates.
|
* Checks for available updates.
|
||||||
*/
|
*/
|
||||||
external fun checkForUpdate(): Array<String>?
|
external fun checkForUpdate(): UpdateResult?
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the URL to the release page
|
|
||||||
*/
|
|
||||||
external fun getUpdateUrl(version: String): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the URL to download the APK for the given version
|
|
||||||
*/
|
|
||||||
external fun getUpdateApkUrl(tag: String, artifact: String, packageId: String): String
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the update checker is enabled through CMAKE options.
|
* Returns whether the update checker is enabled through CMAKE options.
|
||||||
|
|
|
||||||
|
|
@ -175,25 +175,25 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
val latestVersion = NativeLibrary.checkForUpdate()
|
val latestVersion = NativeLibrary.checkForUpdate()
|
||||||
if (latestVersion != null) {
|
if (latestVersion != null) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
val tag: String = latestVersion[0]
|
showUpdateDialog(latestVersion)
|
||||||
val name: String = latestVersion[1]
|
|
||||||
showUpdateDialog(tag, name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showUpdateDialog(tag: String, name: String) {
|
// TODO(crueter): body, "View on Forgejo" button
|
||||||
|
private fun showUpdateDialog(release: NativeLibrary.UpdateResult) {
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(R.string.update_available)
|
.setTitle(R.string.update_available)
|
||||||
.setMessage(getString(R.string.update_available_description, name))
|
.setMessage(getString(R.string.update_available_description, release.title))
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
var artifact = tag
|
val assets = release.assets
|
||||||
// Nightly builds have a slightly different format
|
|
||||||
if (NativeLibrary.isNightlyBuild()) {
|
if (assets.isEmpty()) {
|
||||||
artifact = tag.substringAfter('.', tag)
|
openLink(release.url)
|
||||||
|
} else {
|
||||||
|
downloadAndInstallUpdate(release)
|
||||||
}
|
}
|
||||||
downloadAndInstallUpdate(tag, artifact)
|
|
||||||
}
|
}
|
||||||
.setNeutralButton(R.string.cancel) { dialog, _ ->
|
.setNeutralButton(R.string.cancel) { dialog, _ ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
|
|
@ -206,17 +206,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadAndInstallUpdate(version: String, artifact: String) {
|
private fun openLink(link: String) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, link.toUri())
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadAndInstallUpdate(release: NativeLibrary.UpdateResult) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val packageId = applicationContext.packageName
|
val packageId = applicationContext.packageName
|
||||||
val apkUrl = NativeLibrary.getUpdateApkUrl(version, artifact, packageId)
|
val asset = release.assets[0]
|
||||||
|
val artifact = asset.split("/").last()
|
||||||
val apkFile = File(cacheDir, "update-$artifact.apk")
|
val apkFile = File(cacheDir, "update-$artifact.apk")
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
showDownloadProgressDialog()
|
showDownloadProgressDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloader = APKDownloader(apkUrl, apkFile)
|
val downloader = APKDownloader(asset, apkFile)
|
||||||
downloader.download(
|
downloader.download(
|
||||||
onProgress = { progress ->
|
onProgress = { progress ->
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
|
|
@ -248,7 +254,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
this@MainActivity,
|
this@MainActivity,
|
||||||
getString(R.string.update_download_failed) + "\n\nURL: $apkUrl",
|
getString(R.string.update_download_failed) + "\n\nURL: $asset",
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +283,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
|
|
||||||
private fun updateDownloadProgress(progress: Int) {
|
private fun updateDownloadProgress(progress: Int) {
|
||||||
progressBar?.progress = progress
|
progressBar?.progress = progress
|
||||||
progressMessage?.text = "$progress%"
|
progressMessage?.text = getString(R.string.percent, progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dismissDownloadProgressDialog() {
|
private fun dismissDownloadProgressDialog() {
|
||||||
|
|
|
||||||
|
|
@ -1699,76 +1699,76 @@ JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_isNightlyBuild(
|
||||||
#ifdef ENABLE_UPDATE_CHECKER
|
#ifdef ENABLE_UPDATE_CHECKER
|
||||||
|
|
||||||
|
|
||||||
JNIEXPORT jobjectArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_checkForUpdate(
|
JNIEXPORT jobject JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_checkForUpdate(
|
||||||
JNIEnv* env,
|
JNIEnv* env,
|
||||||
jobject obj) {
|
jobject obj) {
|
||||||
std::optional<UpdateChecker::Update> release = UpdateChecker::GetUpdate();
|
std::optional<Common::Net::Release> release = UpdateChecker::GetUpdate();
|
||||||
if (!release) return nullptr;
|
if (!release) return nullptr;
|
||||||
|
|
||||||
const std::string tag = release->tag;
|
const std::string tag = release->tag;
|
||||||
const std::string name = release->name;
|
const std::string title = release->title;
|
||||||
|
const std::string body = release->body;
|
||||||
|
const std::string url = release->html_url;
|
||||||
|
|
||||||
jobjectArray result = env->NewObjectArray(2, env->FindClass("java/lang/String"), nullptr);
|
// Android *should* only ever define a single asset.
|
||||||
|
// If not, something has gone wrong, but the Kotlin side can handle it.
|
||||||
|
const auto assets = release->GetPlatformAssets();
|
||||||
|
|
||||||
const jstring jtag = env->NewStringUTF(tag.c_str());
|
jclass updateResultClass = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary$UpdateResult");
|
||||||
const jstring jname = env->NewStringUTF(name.c_str());
|
if (!updateResultClass) {
|
||||||
|
LOG_ERROR(Frontend, "Could not find UpdateResult class");
|
||||||
env->SetObjectArrayElement(result, 0, jtag);
|
return nullptr;
|
||||||
env->SetObjectArrayElement(result, 1, jname);
|
|
||||||
env->DeleteLocalRef(jtag);
|
|
||||||
env->DeleteLocalRef(jname);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateUrl(
|
|
||||||
JNIEnv* env,
|
|
||||||
jobject obj,
|
|
||||||
jstring version) {
|
|
||||||
const char* version_str = env->GetStringUTFChars(version, nullptr);
|
|
||||||
const std::string url = fmt::format("{}/{}",
|
|
||||||
std::string{Common::g_build_auto_update_api},
|
|
||||||
version_str);
|
|
||||||
env->ReleaseStringUTFChars(version, version_str);
|
|
||||||
return env->NewStringUTF(url.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateApkUrl(
|
|
||||||
JNIEnv* env,
|
|
||||||
jobject obj,
|
|
||||||
jstring tag,
|
|
||||||
jstring artifact,
|
|
||||||
jstring packageId) {
|
|
||||||
const char* version_str = env->GetStringUTFChars(tag, nullptr);
|
|
||||||
const char* artifact_str = env->GetStringUTFChars(artifact, nullptr);
|
|
||||||
const char* package_id_str = env->GetStringUTFChars(packageId, nullptr);
|
|
||||||
|
|
||||||
std::string variant;
|
|
||||||
std::string package_id(package_id_str);
|
|
||||||
|
|
||||||
if (package_id.find("dev.legacy.eden_emulator") != std::string::npos) {
|
|
||||||
variant = "legacy";
|
|
||||||
} else if (package_id.find("com.miHoYo.Yuanshen") != std::string::npos) {
|
|
||||||
variant = "optimized";
|
|
||||||
} else {
|
|
||||||
#ifdef ARCHITECTURE_arm64
|
|
||||||
variant = "standard";
|
|
||||||
#else
|
|
||||||
variant = "chromeos";
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", artifact_str, variant);
|
jmethodID updateResultCtor = env->GetMethodID(updateResultClass, "<init>", "()V");
|
||||||
|
|
||||||
const std::string url = fmt::format("https://{}/{}/{}",
|
if (!updateResultCtor) {
|
||||||
std::string{Common::g_build_auto_update_api},
|
LOG_ERROR(Frontend, "Could not find UpdateResult ctor");
|
||||||
version_str, apk_filename);
|
env->DeleteLocalRef(updateResultClass);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
env->ReleaseStringUTFChars(tag, version_str);
|
jmethodID setTag = env->GetMethodID(updateResultClass, "setTag", "(Ljava/lang/String;)V");
|
||||||
env->ReleaseStringUTFChars(artifact, artifact_str);
|
jmethodID setTitle = env->GetMethodID(updateResultClass, "setTitle", "(Ljava/lang/String;)V");
|
||||||
env->ReleaseStringUTFChars(packageId, package_id_str);
|
jmethodID setBody = env->GetMethodID(updateResultClass, "setBody", "(Ljava/lang/String;)V");
|
||||||
return env->NewStringUTF(url.c_str());
|
jmethodID setUrl = env->GetMethodID(updateResultClass, "setUrl", "(Ljava/lang/String;)V");
|
||||||
|
jmethodID addAsset = env->GetMethodID(updateResultClass, "addAsset", "(Ljava/lang/String;)V");
|
||||||
|
|
||||||
|
jobject updateResult = env->NewObject(updateResultClass, updateResultCtor);
|
||||||
|
|
||||||
|
LOG_DEBUG(Frontend, "Tag: {}", tag);
|
||||||
|
LOG_DEBUG(Frontend, "Title: {}", title);
|
||||||
|
LOG_DEBUG(Frontend, "Body: {}", body);
|
||||||
|
LOG_DEBUG(Frontend, "Url: {}", url);
|
||||||
|
|
||||||
|
const auto jtag = env->NewStringUTF(tag.c_str());
|
||||||
|
const auto jtitle = env->NewStringUTF(title.c_str());
|
||||||
|
const auto jbody = env->NewStringUTF(body.c_str());
|
||||||
|
const auto jurl = env->NewStringUTF(url.c_str());
|
||||||
|
|
||||||
|
env->CallVoidMethod(updateResult, setTag, jtag);
|
||||||
|
env->CallVoidMethod(updateResult, setTitle, jtitle);
|
||||||
|
env->CallVoidMethod(updateResult, setBody, jbody);
|
||||||
|
env->CallVoidMethod(updateResult, setUrl, jurl);
|
||||||
|
|
||||||
|
// TODO(crueter): Handling for multiple assets?
|
||||||
|
// Maybe another data class x(
|
||||||
|
for (const Common::Net::Asset &a : assets) {
|
||||||
|
const auto jaurl = env->NewStringUTF(a.path.c_str());
|
||||||
|
env->CallVoidMethod(updateResult, addAsset, jaurl);
|
||||||
|
env->DeleteLocalRef(jaurl);
|
||||||
|
}
|
||||||
|
|
||||||
|
env->DeleteLocalRef(jtag);
|
||||||
|
env->DeleteLocalRef(jtitle);
|
||||||
|
env->DeleteLocalRef(jbody);
|
||||||
|
env->DeleteLocalRef(jurl);
|
||||||
|
|
||||||
|
env->DeleteLocalRef(updateResultClass);
|
||||||
|
|
||||||
|
return updateResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getBuildVersion(
|
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getBuildVersion(
|
||||||
|
|
|
||||||
|
|
@ -1783,5 +1783,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
<string name="external_content">External Content</string>
|
<string name="external_content">External Content</string>
|
||||||
<string name="add_folders">Add Folder</string>
|
<string name="add_folders">Add Folder</string>
|
||||||
|
<string name="percent">%1$d%%</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,8 @@ add_library(
|
||||||
zstd_compression.h
|
zstd_compression.h
|
||||||
fs/ryujinx_compat.h fs/ryujinx_compat.cpp
|
fs/ryujinx_compat.h fs/ryujinx_compat.cpp
|
||||||
fs/symlink.h fs/symlink.cpp
|
fs/symlink.h fs/symlink.cpp
|
||||||
httplib.h)
|
httplib.h
|
||||||
|
net/net.h net/net.cpp)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_sources(common PRIVATE windows/timer_resolution.cpp
|
target_sources(common PRIVATE windows/timer_resolution.cpp
|
||||||
|
|
@ -245,7 +246,7 @@ else()
|
||||||
target_link_libraries(common PUBLIC Boost::headers)
|
target_link_libraries(common PUBLIC Boost::headers)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_link_libraries(common PUBLIC Boost::filesystem Boost::context httplib::httplib)
|
target_link_libraries(common PUBLIC Boost::filesystem Boost::context httplib::httplib nlohmann_json::nlohmann_json)
|
||||||
|
|
||||||
if (lz4_ADDED)
|
if (lz4_ADDED)
|
||||||
target_include_directories(common PRIVATE ${lz4_SOURCE_DIR}/lib)
|
target_include_directories(common PRIVATE ${lz4_SOURCE_DIR}/lib)
|
||||||
|
|
|
||||||
287
src/common/net/net.cpp
Normal file
287
src/common/net/net.cpp
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <boost/algorithm/string/classification.hpp>
|
||||||
|
#include <boost/algorithm/string/replace.hpp>
|
||||||
|
#include <boost/algorithm/string/split.hpp>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#include "common/scm_rev.h"
|
||||||
|
#include "net.h"
|
||||||
|
|
||||||
|
#include "common/logging.h"
|
||||||
|
|
||||||
|
#include "common/httplib.h"
|
||||||
|
|
||||||
|
#ifdef YUZU_BUNDLED_OPENSSL
|
||||||
|
#include <openssl/cert.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define QT_TR_NOOP(x) x
|
||||||
|
|
||||||
|
namespace Common::Net {
|
||||||
|
|
||||||
|
std::vector<Asset> Release::GetPlatformAssets() const {
|
||||||
|
// TODO(crueter): Need better handling for this as a whole.
|
||||||
|
#ifdef NIGHTLY_BUILD
|
||||||
|
std::vector<std::string> result;
|
||||||
|
boost::algorithm::split(result, tag, boost::is_any_of("."));
|
||||||
|
if (result.size() != 2)
|
||||||
|
return {};
|
||||||
|
const auto ref = result.at(1);
|
||||||
|
#else
|
||||||
|
const auto ref = tag;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::vector<Asset> found_assets;
|
||||||
|
|
||||||
|
// FIXME: This is mildly inefficient.
|
||||||
|
// Finds assets based on a hierarchy of regex search strings.
|
||||||
|
const auto find_asset = [&found_assets, ref, this](const std::string& name,
|
||||||
|
const std::vector<std::string>& suffixes) {
|
||||||
|
for (const std::string& asset : assets) {
|
||||||
|
for (const auto& suffix : suffixes) {
|
||||||
|
if (asset.ends_with(suffix)) {
|
||||||
|
const std::string_view asset_sv = asset;
|
||||||
|
const size_t pos = asset_sv.find_last_of('/');
|
||||||
|
const std::string_view filename =
|
||||||
|
(pos != std::string_view::npos) ? asset_sv.substr(pos + 1) : asset_sv;
|
||||||
|
|
||||||
|
found_assets.emplace_back(Asset{
|
||||||
|
.name = name,
|
||||||
|
.url = host,
|
||||||
|
.path = asset,
|
||||||
|
.filename = std::string{filename},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#ifdef ARCHITECTURE_x86_64
|
||||||
|
find_asset("Standard", {"amd64-msvc-standard.exe", "amd64-msvc-standard.zip", "mingw-amd64-gcc-standard.exe", "mingw-amd64-gcc-standard.zip"});
|
||||||
|
find_asset("PGO", {"mingw-amd64-clang-pgo.exe", "mingw-amd64-clang-pgo.zip"});
|
||||||
|
#elif defined(ARCHITECTURE_arm64)
|
||||||
|
find_asset("Standard", {"mingw-arm64-clang-standard.exe", "mingw-arm64-clang-standard.zip"});
|
||||||
|
find_asset("PGO", {"mingw-arm64-clang-pgo.exe", "mingw-arm64-clang-pgo.zip"});
|
||||||
|
#endif
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
#ifdef ARCHITECTURE_arm64
|
||||||
|
find_asset("Standard", {".dmg", ".tar.gz"});
|
||||||
|
#endif
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
#ifdef ARCHITECTURE_x86_64
|
||||||
|
find_asset("Standard", {"chromeos.apk"});
|
||||||
|
#elif defined(ARCHITECTURE_arm64)
|
||||||
|
#ifdef YUZU_LEGACY
|
||||||
|
find_asset("Standard", {"legacy.apk"});
|
||||||
|
#elif defined(GENSHIN_SPOOF)
|
||||||
|
find_asset("Standard", {"optimized.apk"});
|
||||||
|
#else
|
||||||
|
find_asset("Standard", {"standard.apk"});
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
return found_assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline u64 ParseIsoTimestamp(const std::string& iso) {
|
||||||
|
if (iso.empty())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
std::string buf = iso;
|
||||||
|
if (buf.back() == 'Z')
|
||||||
|
buf.pop_back();
|
||||||
|
|
||||||
|
std::tm tm{};
|
||||||
|
std::istringstream ss(buf);
|
||||||
|
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
|
||||||
|
if (ss.fail())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
return static_cast<u64>(_mkgmtime(&tm));
|
||||||
|
#else
|
||||||
|
return static_cast<u64>(timegm(&tm));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Release> Release::FromJson(const nlohmann::json& json, const std::string& host,
|
||||||
|
const std::string& repo) {
|
||||||
|
Release rel;
|
||||||
|
if (!json.is_object())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
rel.tag = json.value("tag_name", std::string{});
|
||||||
|
if (rel.tag.empty())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
rel.title = json.value("name", rel.tag);
|
||||||
|
rel.id = json.value("id", std::hash<std::string>{}(rel.title));
|
||||||
|
|
||||||
|
rel.published = ParseIsoTimestamp(json.value("published_at", std::string{}));
|
||||||
|
rel.prerelease = json.value("prerelease", false);
|
||||||
|
|
||||||
|
auto body = json.value("body", rel.title);
|
||||||
|
boost::replace_all(body, "\\r", "");
|
||||||
|
boost::replace_all(body, "\\n", "\n");
|
||||||
|
rel.body = body;
|
||||||
|
|
||||||
|
rel.host = host;
|
||||||
|
|
||||||
|
const auto release_base =
|
||||||
|
fmt::format("{}/{}/releases", Common::g_build_auto_update_website, repo);
|
||||||
|
const auto fallback_html = fmt::format("{}/tag/{}", release_base, rel.tag);
|
||||||
|
rel.html_url = json.value("html_url", fallback_html);
|
||||||
|
|
||||||
|
// This is our own "fake" API.
|
||||||
|
if (json.contains("base")) {
|
||||||
|
const auto base = json.value("base", fmt::format("https://{}", Common::g_build_auto_update_api));
|
||||||
|
rel.base_download_url = fmt::format("{}/{}", base, rel.tag);
|
||||||
|
|
||||||
|
// Assets are easy :)
|
||||||
|
rel.assets = json.value("assets", std::vector<std::string>{});
|
||||||
|
} else {
|
||||||
|
const auto base_download_url = fmt::format("/{}/releases/download/{}", repo, rel.tag);
|
||||||
|
|
||||||
|
rel.base_download_url = base_download_url;
|
||||||
|
|
||||||
|
// assets are a bit more complex here. :(
|
||||||
|
std::vector<std::string> assets;
|
||||||
|
const nlohmann::json& arr = json["assets"];
|
||||||
|
for (const auto &obj : arr) {
|
||||||
|
const auto url = obj.value("browser_download_url", std::string{});
|
||||||
|
assets.emplace_back(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
rel.assets = assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rel;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Release> Release::FromJson(const std::string_view& json, const std::string& host,
|
||||||
|
const std::string& repo) {
|
||||||
|
try {
|
||||||
|
return FromJson(nlohmann::json::parse(json), host, repo);
|
||||||
|
} catch (std::exception& e) {
|
||||||
|
LOG_WARNING(Common, "Failed to parse JSON: {}", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Release> Release::ListFromJson(const nlohmann::json& json, const std::string& host,
|
||||||
|
const std::string& repo) {
|
||||||
|
if (!json.is_array())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
std::vector<Release> releases;
|
||||||
|
for (const auto& obj : json) {
|
||||||
|
auto rel = Release::FromJson(obj, host, repo);
|
||||||
|
if (rel)
|
||||||
|
releases.emplace_back(rel.value());
|
||||||
|
}
|
||||||
|
return releases;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Release> Release::ListFromJson(const std::string_view& json, const std::string& host,
|
||||||
|
const std::string& repo) {
|
||||||
|
try {
|
||||||
|
return ListFromJson(nlohmann::json::parse(json), host, repo);
|
||||||
|
} catch (std::exception& e) {
|
||||||
|
LOG_WARNING(Common, "Failed to parse JSON: {}", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> MakeRequest(const std::string& url, const std::string& path) {
|
||||||
|
try {
|
||||||
|
constexpr std::size_t timeout_seconds = 15;
|
||||||
|
|
||||||
|
std::unique_ptr<httplib::Client> client = std::make_unique<httplib::Client>(url);
|
||||||
|
client->set_connection_timeout(timeout_seconds);
|
||||||
|
client->set_read_timeout(timeout_seconds);
|
||||||
|
client->set_write_timeout(timeout_seconds);
|
||||||
|
|
||||||
|
#ifdef YUZU_BUNDLED_OPENSSL
|
||||||
|
client->load_ca_cert_store(kCert, sizeof(kCert));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (client == nullptr) {
|
||||||
|
LOG_ERROR(Common, "Invalid URL {}{}", url, path);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
httplib::Request request{
|
||||||
|
.method = "GET",
|
||||||
|
.path = path,
|
||||||
|
};
|
||||||
|
|
||||||
|
client->set_follow_location(true);
|
||||||
|
httplib::Result result = client->send(request);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
LOG_ERROR(Common, "GET to {}{} returned null", url, path);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& response = result.value();
|
||||||
|
if (response.status >= 400) {
|
||||||
|
LOG_ERROR(Common, "GET to {}{} returned error status code: {}", url, path,
|
||||||
|
response.status);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!response.headers.contains("content-type")) {
|
||||||
|
LOG_ERROR(Common, "GET to {}{} returned no content", url, path);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.body;
|
||||||
|
} catch (std::exception& e) {
|
||||||
|
LOG_ERROR(Common, "GET to {}{} failed during update check: {}", url, path, e.what());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Release> GetReleases() {
|
||||||
|
const auto body = GetReleasesBody();
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
LOG_WARNING(Common, "Failed to get stable releases");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string_view body_str = body.value();
|
||||||
|
const auto url = fmt::format("https://{}", Common::g_build_auto_update_stable_api);
|
||||||
|
return Release::ListFromJson(body_str, url, Common::g_build_auto_update_stable_repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Release> GetLatestRelease() {
|
||||||
|
const auto releases_path = Common::g_build_auto_update_api_path;
|
||||||
|
const auto url = fmt::format("https://{}", Common::g_build_auto_update_api);
|
||||||
|
|
||||||
|
const auto body = MakeRequest(url, releases_path);
|
||||||
|
if (!body) {
|
||||||
|
LOG_WARNING(Common, "Failed to get latest release");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string_view body_str = body.value();
|
||||||
|
return Release::FromJson(body_str, url, Common::g_build_auto_update_repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> GetReleasesBody() {
|
||||||
|
const auto releases_path =
|
||||||
|
fmt::format("/{}/{}/releases", Common::g_build_auto_update_stable_api_path,
|
||||||
|
Common::g_build_auto_update_stable_repo);
|
||||||
|
const auto url = fmt::format("https://{}", Common::g_build_auto_update_stable_api);
|
||||||
|
|
||||||
|
return MakeRequest(url, releases_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common::Net
|
||||||
56
src/common/net/net.h
Normal file
56
src/common/net/net.h
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace Common::Net {
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
std::string name;
|
||||||
|
std::string url;
|
||||||
|
std::string path;
|
||||||
|
std::string filename;
|
||||||
|
} Asset;
|
||||||
|
|
||||||
|
typedef struct Release {
|
||||||
|
std::string title;
|
||||||
|
std::string body;
|
||||||
|
std::string tag;
|
||||||
|
std::string base_download_url;
|
||||||
|
std::string html_url;
|
||||||
|
std::string host;
|
||||||
|
|
||||||
|
std::vector<std::string> assets;
|
||||||
|
|
||||||
|
u64 id;
|
||||||
|
u64 published;
|
||||||
|
bool prerelease;
|
||||||
|
|
||||||
|
// Get the relevant list of assets for the current platform.
|
||||||
|
std::vector<Asset> GetPlatformAssets() const;
|
||||||
|
|
||||||
|
static std::optional<Release> FromJson(const nlohmann::json& json, const std::string &host, const std::string& repo);
|
||||||
|
static std::optional<Release> FromJson(const std::string_view& json, const std::string &host, const std::string& repo);
|
||||||
|
static std::vector<Release> ListFromJson(const nlohmann::json &json, const std::string &host, const std::string &repo);
|
||||||
|
static std::vector<Release> ListFromJson(const std::string_view &json, const std::string &host, const std::string &repo);
|
||||||
|
} Release;
|
||||||
|
|
||||||
|
// Make a request via httplib, and return the response body if applicable.
|
||||||
|
std::optional<std::string> MakeRequest(const std::string &url, const std::string &path);
|
||||||
|
|
||||||
|
// Get all of the latest stable releases.
|
||||||
|
std::vector<Release> GetReleases();
|
||||||
|
|
||||||
|
// Get all of the latest stable releases as text.
|
||||||
|
std::optional<std::string> GetReleasesBody();
|
||||||
|
|
||||||
|
// Get the latest release of the current channel.
|
||||||
|
std::optional<Release> GetLatestRelease();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,9 @@
|
||||||
#define BUILD_AUTO_UPDATE_API "@BUILD_AUTO_UPDATE_API@"
|
#define BUILD_AUTO_UPDATE_API "@BUILD_AUTO_UPDATE_API@"
|
||||||
#define BUILD_AUTO_UPDATE_API_PATH "@BUILD_AUTO_UPDATE_API_PATH@"
|
#define BUILD_AUTO_UPDATE_API_PATH "@BUILD_AUTO_UPDATE_API_PATH@"
|
||||||
#define BUILD_AUTO_UPDATE_REPO "@BUILD_AUTO_UPDATE_REPO@"
|
#define BUILD_AUTO_UPDATE_REPO "@BUILD_AUTO_UPDATE_REPO@"
|
||||||
|
#define BUILD_AUTO_UPDATE_STABLE_API "@BUILD_AUTO_UPDATE_STABLE_API@"
|
||||||
|
#define BUILD_AUTO_UPDATE_STABLE_API_PATH "@BUILD_AUTO_UPDATE_STABLE_API_PATH@"
|
||||||
|
#define BUILD_AUTO_UPDATE_STABLE_REPO "@BUILD_AUTO_UPDATE_STABLE_REPO@"
|
||||||
#define IS_NIGHTLY_BUILD @IS_NIGHTLY_BUILD@
|
#define IS_NIGHTLY_BUILD @IS_NIGHTLY_BUILD@
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
|
|
@ -45,5 +48,8 @@ constexpr const char g_build_auto_update_website[] = BUILD_AUTO_UPDATE_WEBSITE;
|
||||||
constexpr const char g_build_auto_update_api[] = BUILD_AUTO_UPDATE_API;
|
constexpr const char g_build_auto_update_api[] = BUILD_AUTO_UPDATE_API;
|
||||||
constexpr const char g_build_auto_update_api_path[] = BUILD_AUTO_UPDATE_API_PATH;
|
constexpr const char g_build_auto_update_api_path[] = BUILD_AUTO_UPDATE_API_PATH;
|
||||||
constexpr const char g_build_auto_update_repo[] = BUILD_AUTO_UPDATE_REPO;
|
constexpr const char g_build_auto_update_repo[] = BUILD_AUTO_UPDATE_REPO;
|
||||||
|
constexpr const char g_build_auto_update_stable_api[] = BUILD_AUTO_UPDATE_STABLE_API;
|
||||||
|
constexpr const char g_build_auto_update_stable_api_path[] = BUILD_AUTO_UPDATE_STABLE_API_PATH;
|
||||||
|
constexpr const char g_build_auto_update_stable_repo[] = BUILD_AUTO_UPDATE_STABLE_REPO;
|
||||||
|
|
||||||
} // namespace Common
|
} // namespace Common
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,8 @@ extern const char g_build_auto_update_website[];
|
||||||
extern const char g_build_auto_update_api[];
|
extern const char g_build_auto_update_api[];
|
||||||
extern const char g_build_auto_update_api_path[];
|
extern const char g_build_auto_update_api_path[];
|
||||||
extern const char g_build_auto_update_repo[];
|
extern const char g_build_auto_update_repo[];
|
||||||
|
extern const char g_build_auto_update_stable_api[];
|
||||||
|
extern const char g_build_auto_update_stable_api_path[];
|
||||||
|
extern const char g_build_auto_update_stable_repo[];
|
||||||
|
|
||||||
} // namespace Common
|
} // namespace Common
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "common/net/net.h"
|
||||||
|
#include "common/scm_rev.h"
|
||||||
#include "core/hle/service/bcat/news/builtin_news.h"
|
#include "core/hle/service/bcat/news/builtin_news.h"
|
||||||
#include "core/hle/service/bcat/news/msgpack.h"
|
#include "core/hle/service/bcat/news/msgpack.h"
|
||||||
#include "core/hle/service/bcat/news/news_storage.h"
|
#include "core/hle/service/bcat/news/news_storage.h"
|
||||||
|
|
@ -22,10 +24,8 @@
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <future>
|
#include <future>
|
||||||
#include <iomanip>
|
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <sstream>
|
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#ifdef YUZU_BUNDLED_OPENSSL
|
#ifdef YUZU_BUNDLED_OPENSSL
|
||||||
|
|
@ -35,9 +35,6 @@
|
||||||
namespace Service::News {
|
namespace Service::News {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// TODO(crueter): COMPILE DEFINITION
|
|
||||||
constexpr const char* GitHubAPI_EdenReleases = "/api/v1/repos/eden-emu/eden/releases";
|
|
||||||
|
|
||||||
// Cached logo data
|
// Cached logo data
|
||||||
std::vector<u8> default_logo_small;
|
std::vector<u8> default_logo_small;
|
||||||
std::vector<u8> default_logo_large;
|
std::vector<u8> default_logo_large;
|
||||||
|
|
@ -66,24 +63,6 @@ u32 HashToNewsId(std::string_view key) {
|
||||||
return static_cast<u32>(std::hash<std::string_view>{}(key) & 0x7FFFFFFF);
|
return static_cast<u32>(std::hash<std::string_view>{}(key) & 0x7FFFFFFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
u64 ParseIsoTimestamp(const std::string& iso) {
|
|
||||||
if (iso.empty()) return 0;
|
|
||||||
|
|
||||||
std::string buf = iso;
|
|
||||||
if (buf.back() == 'Z') buf.pop_back();
|
|
||||||
|
|
||||||
std::tm tm{};
|
|
||||||
std::istringstream ss(buf);
|
|
||||||
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
|
|
||||||
if (ss.fail()) return 0;
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
return static_cast<u64>(_mkgmtime(&tm));
|
|
||||||
#else
|
|
||||||
return static_cast<u64>(timegm(&tm));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<u8> TryLoadFromDisk(const std::filesystem::path& path) {
|
std::vector<u8> TryLoadFromDisk(const std::filesystem::path& path) {
|
||||||
if (!std::filesystem::exists(path)) return {};
|
if (!std::filesystem::exists(path)) return {};
|
||||||
|
|
||||||
|
|
@ -100,8 +79,9 @@ std::vector<u8> TryLoadFromDisk(const std::filesystem::path& path) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(crueter): Migrate to use Common::Net
|
||||||
std::vector<u8> DownloadImage(const std::string& url_path, const std::filesystem::path& cache_path) {
|
std::vector<u8> DownloadImage(const std::string& url_path, const std::filesystem::path& cache_path) {
|
||||||
LOG_INFO(Service_BCAT, "Downloading image: https://eden-emu.dev{}", url_path);
|
LOG_DEBUG(Service_BCAT, "Downloading image: https://eden-emu.dev{}", url_path);
|
||||||
try {
|
try {
|
||||||
httplib::Client cli("https://eden-emu.dev");
|
httplib::Client cli("https://eden-emu.dev");
|
||||||
cli.set_follow_location(true);
|
cli.set_follow_location(true);
|
||||||
|
|
@ -226,67 +206,6 @@ void WriteCachedJson(std::string_view json) {
|
||||||
(void)Common::FS::WriteStringToFile(path, Common::FS::FileType::TextFile, json);
|
(void)Common::FS::WriteStringToFile(path, Common::FS::FileType::TextFile, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> DownloadReleasesJson() {
|
|
||||||
try {
|
|
||||||
#ifdef YUZU_BUNDLED_OPENSSL
|
|
||||||
const auto url = "https://git.eden-emu.dev";
|
|
||||||
#else
|
|
||||||
const auto url = "git.eden-emu.dev";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// TODO(crueter): This is duplicated between frontend and here.
|
|
||||||
constexpr auto path = GitHubAPI_EdenReleases;
|
|
||||||
|
|
||||||
constexpr std::size_t timeout_seconds = 15;
|
|
||||||
|
|
||||||
std::unique_ptr<httplib::Client> client = std::make_unique<httplib::Client>(url);
|
|
||||||
client->set_connection_timeout(timeout_seconds);
|
|
||||||
client->set_read_timeout(timeout_seconds);
|
|
||||||
client->set_write_timeout(timeout_seconds);
|
|
||||||
|
|
||||||
#ifdef YUZU_BUNDLED_OPENSSL
|
|
||||||
client->load_ca_cert_store(kCert, sizeof(kCert));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (client == nullptr) {
|
|
||||||
LOG_ERROR(Service_BCAT, "Invalid URL {}{}", url, path);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
httplib::Request request{
|
|
||||||
.method = "GET",
|
|
||||||
.path = path,
|
|
||||||
};
|
|
||||||
|
|
||||||
client->set_follow_location(true);
|
|
||||||
httplib::Result result = client->send(request);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
LOG_ERROR(Service_BCAT, "GET to {}{} returned null", url, path);
|
|
||||||
return {};
|
|
||||||
} else if (result->status < 400) {
|
|
||||||
return result->body;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result->status >= 400) {
|
|
||||||
LOG_ERROR(Service_BCAT,
|
|
||||||
"GET to {}{} returned error status code: {}",
|
|
||||||
url,
|
|
||||||
path,
|
|
||||||
result->status);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result->headers.contains("content-type")) {
|
|
||||||
LOG_ERROR(Service_BCAT, "GET to {}{} returned no content", url, path);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
} catch (...) {
|
|
||||||
LOG_WARNING(Service_BCAT, " failed to download releases");
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// idk but News App does not render Markdown or HTML, so remove some formatting.
|
// idk but News App does not render Markdown or HTML, so remove some formatting.
|
||||||
std::string SanitizeMarkdown(std::string_view markdown) {
|
std::string SanitizeMarkdown(std::string_view markdown) {
|
||||||
std::string result;
|
std::string result;
|
||||||
|
|
@ -342,9 +261,7 @@ std::string SanitizeMarkdown(std::string_view markdown) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string FormatBody(const nlohmann::json& release, std::string_view title) {
|
std::string FormatBody(std::string body, const std::string_view &title) {
|
||||||
std::string body = release.value("body", std::string{});
|
|
||||||
|
|
||||||
if (body.empty()) {
|
if (body.empty()) {
|
||||||
return std::string(title);
|
return std::string(title);
|
||||||
}
|
}
|
||||||
|
|
@ -375,52 +292,32 @@ std::string FormatBody(const nlohmann::json& release, std::string_view title) {
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImportReleases(std::string_view json_text) {
|
void ImportReleases(const std::vector<Common::Net::Release> &releases) {
|
||||||
nlohmann::json root;
|
|
||||||
try {
|
|
||||||
root = nlohmann::json::parse(json_text);
|
|
||||||
} catch (...) {
|
|
||||||
LOG_WARNING(Service_BCAT, "failed to parse JSON");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root.is_array()) return;
|
|
||||||
|
|
||||||
std::vector<u32> news_ids;
|
std::vector<u32> news_ids;
|
||||||
for (const auto& rel : root) {
|
for (const auto& rel : releases) {
|
||||||
if (!rel.is_object()) continue;
|
const u32 news_id = u32(rel.id & 0x7FFFFFFF);
|
||||||
std::string title = rel.value("name", rel.value("tag_name", std::string{}));
|
|
||||||
if (title.empty()) continue;
|
|
||||||
|
|
||||||
const u64 release_id = rel.value("id", 0);
|
|
||||||
const u32 news_id = release_id ? static_cast<u32>(release_id & 0x7FFFFFFF) : HashToNewsId(title);
|
|
||||||
news_ids.push_back(news_id);
|
news_ids.push_back(news_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
PreloadNewsImages(news_ids);
|
PreloadNewsImages(news_ids);
|
||||||
|
|
||||||
for (const auto& rel : root) {
|
for (const auto& rel : releases) {
|
||||||
if (!rel.is_object()) continue;
|
const std::string title = rel.title;
|
||||||
|
const std::string body = rel.body;
|
||||||
|
const std::string html_url = rel.html_url;
|
||||||
|
|
||||||
std::string title = rel.value("name", rel.value("tag_name", std::string{}));
|
const u32 news_id = u32(rel.id & 0x7FFFFFFF);
|
||||||
if (title.empty()) continue;
|
const u64 published = rel.published;
|
||||||
|
|
||||||
const u64 release_id = rel.value("id", 0);
|
|
||||||
const u32 news_id = release_id ? static_cast<u32>(release_id & 0x7FFFFFFF) : HashToNewsId(title);
|
|
||||||
const u64 published = ParseIsoTimestamp(rel.value("published_at", std::string{}));
|
|
||||||
const u64 pickup_limit = published + 600000000;
|
const u64 pickup_limit = published + 600000000;
|
||||||
const u32 priority = rel.value("prerelease", false) ? 1500 : 2500;
|
const u32 priority = rel.prerelease ? 1500 : 2500;
|
||||||
|
|
||||||
std::string author = "eden";
|
std::string author = "Eden";
|
||||||
if (rel.contains("author") && rel["author"].is_object()) {
|
|
||||||
author = rel["author"].value("login", "eden");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto payload = BuildMsgpack(title, FormatBody(rel, title), title, published,
|
auto payload = BuildMsgpack(title, FormatBody(body, title), title, published,
|
||||||
pickup_limit, priority, {"en"}, author, {},
|
pickup_limit, priority, {"en"}, author, {},
|
||||||
rel.value("html_url", std::string{}), news_id);
|
html_url, news_id);
|
||||||
|
|
||||||
const std::string news_id_str = fmt::format("LA{:020}", news_id);
|
const std::string news_id_str = fmt::format("LA{:020}", rel.id);
|
||||||
|
|
||||||
GithubNewsMeta meta{
|
GithubNewsMeta meta{
|
||||||
.news_id = news_id_str,
|
.news_id = news_id_str,
|
||||||
|
|
@ -565,15 +462,21 @@ void EnsureBuiltinNewsLoaded() {
|
||||||
LoadDefaultLogos();
|
LoadDefaultLogos();
|
||||||
|
|
||||||
if (const auto cached = ReadCachedJson()) {
|
if (const auto cached = ReadCachedJson()) {
|
||||||
ImportReleases(*cached);
|
const std::string_view body = cached.value();
|
||||||
LOG_DEBUG(Service_BCAT, "news: {} entries loaded from cache", NewsStorage::Instance().ListAll().size());
|
const auto releases = Common::Net::Release::ListFromJson(body, Common::g_build_auto_update_stable_api, Common::g_build_auto_update_stable_repo);
|
||||||
|
ImportReleases(releases);
|
||||||
|
|
||||||
|
LOG_INFO(Service_BCAT, "news: {} entries loaded from cache", NewsStorage::Instance().ListAll().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::thread([] {
|
std::thread([] {
|
||||||
if (const auto fresh = DownloadReleasesJson()) {
|
if (const auto fresh = Common::Net::GetReleasesBody()) {
|
||||||
WriteCachedJson(*fresh);
|
const std::string_view body = fresh.value();
|
||||||
ImportReleases(*fresh);
|
WriteCachedJson(body);
|
||||||
LOG_DEBUG(Service_BCAT, "news: {} entries updated from Forgejo", NewsStorage::Instance().ListAll().size());
|
const auto releases = Common::Net::Release::ListFromJson(body, Common::g_build_auto_update_stable_api, Common::g_build_auto_update_stable_repo);
|
||||||
|
ImportReleases(releases);
|
||||||
|
|
||||||
|
LOG_INFO(Service_BCAT, "news: {} entries updated from Forgejo", NewsStorage::Instance().ListAll().size());
|
||||||
}
|
}
|
||||||
}).detach();
|
}).detach();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,146 +5,45 @@
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#ifdef NIGHTLY_BUILD
|
||||||
#include <boost/algorithm/string/classification.hpp>
|
#include <boost/algorithm/string/classification.hpp>
|
||||||
#include <boost/algorithm/string/split.hpp>
|
#include <boost/algorithm/string/split.hpp>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
#include "common/logging.h"
|
#include "common/net/net.h"
|
||||||
#include "common/scm_rev.h"
|
#include "common/scm_rev.h"
|
||||||
#include "update_checker.h"
|
#include "update_checker.h"
|
||||||
|
|
||||||
#include "common/httplib.h"
|
#include "common/logging.h"
|
||||||
|
|
||||||
#ifdef YUZU_BUNDLED_OPENSSL
|
std::optional<Common::Net::Release> UpdateChecker::GetUpdate() {
|
||||||
#include <openssl/cert.h>
|
const auto latest = Common::Net::GetLatestRelease();
|
||||||
#endif
|
if (!latest) return std::nullopt;
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
LOG_INFO(Frontend, "Received update {}", latest->title);
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
std::optional<std::string> UpdateChecker::GetResponse(std::string url, std::string path)
|
#ifdef NIGHTLY_BUILD
|
||||||
{
|
std::vector<std::string> result;
|
||||||
try {
|
|
||||||
constexpr std::size_t timeout_seconds = 15;
|
|
||||||
|
|
||||||
std::unique_ptr<httplib::Client> client = std::make_unique<httplib::Client>(url);
|
boost::split(result, latest->tag, boost::is_any_of("."));
|
||||||
client->set_connection_timeout(timeout_seconds);
|
if (result.size() != 2)
|
||||||
client->set_read_timeout(timeout_seconds);
|
|
||||||
client->set_write_timeout(timeout_seconds);
|
|
||||||
|
|
||||||
#ifdef YUZU_BUNDLED_OPENSSL
|
|
||||||
client->load_ca_cert_store(kCert, sizeof(kCert));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (client == nullptr) {
|
|
||||||
LOG_ERROR(Frontend, "Invalid URL {}{}", url, path);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
httplib::Request request{
|
|
||||||
.method = "GET",
|
|
||||||
.path = path,
|
|
||||||
};
|
|
||||||
|
|
||||||
client->set_follow_location(true);
|
|
||||||
httplib::Result result = client->send(request);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
LOG_ERROR(Frontend, "GET to {}{} returned null", url, path);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto &response = result.value();
|
|
||||||
if (response.status >= 400) {
|
|
||||||
LOG_ERROR(Frontend,
|
|
||||||
"GET to {}{} returned error status code: {}",
|
|
||||||
url,
|
|
||||||
path,
|
|
||||||
response.status);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
if (!response.headers.contains("content-type")) {
|
|
||||||
LOG_ERROR(Frontend, "GET to {}{} returned no content", url, path);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body;
|
|
||||||
} catch (std::exception &e) {
|
|
||||||
LOG_ERROR(Frontend,
|
|
||||||
"GET to {}{} failed during update check: {}",
|
|
||||||
url,
|
|
||||||
path,
|
|
||||||
e.what());
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<UpdateChecker::Update> UpdateChecker::GetLatestRelease() {
|
const std::string tag = result[1];
|
||||||
#ifdef YUZU_BUNDLED_OPENSSL
|
|
||||||
const auto update_check_url = fmt::format("https://{}", Common::g_build_auto_update_api);
|
boost::split(result, std::string{Common::g_build_version}, boost::is_any_of("-"));
|
||||||
|
if (result.empty())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
const std::string build = result[0];
|
||||||
#else
|
#else
|
||||||
const auto update_check_url = std::string{Common::g_build_auto_update_api};
|
const std::string tag = latest->tag;
|
||||||
|
const std::string build = Common::g_build_version;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
auto update_check_path = std::string{Common::g_build_auto_update_api_path};
|
if (tag != build)
|
||||||
try {
|
return latest;
|
||||||
const auto response = UpdateChecker::GetResponse(update_check_url, update_check_path);
|
|
||||||
|
|
||||||
if (!response)
|
|
||||||
return {};
|
|
||||||
|
|
||||||
const std::string latest_tag = nlohmann::json::parse(response.value()).at("tag_name");
|
|
||||||
const std::string latest_name = nlohmann::json::parse(response.value()).at("name");
|
|
||||||
|
|
||||||
return Update{latest_tag, latest_name};
|
|
||||||
} catch (nlohmann::detail::out_of_range&) {
|
|
||||||
LOG_ERROR(Frontend,
|
|
||||||
"Parsing JSON response from {}{} failed during update check: "
|
|
||||||
"nlohmann::detail::out_of_range",
|
|
||||||
update_check_url,
|
|
||||||
update_check_path);
|
|
||||||
return {};
|
|
||||||
} catch (nlohmann::detail::type_error&) {
|
|
||||||
LOG_ERROR(Frontend,
|
|
||||||
"Parsing JSON response from {}{} failed during update check: "
|
|
||||||
"nlohmann::detail::type_error",
|
|
||||||
update_check_url,
|
|
||||||
update_check_path);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<UpdateChecker::Update> UpdateChecker::GetUpdate() {
|
|
||||||
const std::optional<UpdateChecker::Update> latest_release_tag =
|
|
||||||
UpdateChecker::GetLatestRelease();
|
|
||||||
|
|
||||||
if (!latest_release_tag)
|
|
||||||
goto empty;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::string tag, build;
|
|
||||||
if (Common::g_is_nightly_build) {
|
|
||||||
std::vector<std::string> result;
|
|
||||||
|
|
||||||
boost::split(result, latest_release_tag->tag, boost::is_any_of("."));
|
|
||||||
if (result.size() != 2)
|
|
||||||
goto empty;
|
|
||||||
tag = result[1];
|
|
||||||
|
|
||||||
boost::split(result, std::string{Common::g_build_version}, boost::is_any_of("-"));
|
|
||||||
if (result.empty())
|
|
||||||
goto empty;
|
|
||||||
build = result[0];
|
|
||||||
} else {
|
|
||||||
tag = latest_release_tag->tag;
|
|
||||||
build = Common::g_build_version;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tag != build)
|
|
||||||
return latest_release_tag.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
empty:
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,10 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include "common/net/net.h"
|
||||||
|
|
||||||
namespace UpdateChecker {
|
namespace UpdateChecker {
|
||||||
|
|
||||||
typedef struct {
|
std::optional<Common::Net::Release> GetUpdate();
|
||||||
std::string tag;
|
|
||||||
std::string name;
|
|
||||||
} Update;
|
|
||||||
|
|
||||||
std::optional<std::string> GetResponse(std::string url, std::string path);
|
|
||||||
std::optional<Update> GetLatestRelease();
|
|
||||||
std::optional<Update> GetUpdate();
|
|
||||||
} // namespace UpdateChecker
|
} // namespace UpdateChecker
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,8 @@ add_executable(yuzu
|
||||||
|
|
||||||
render/performance_overlay.h render/performance_overlay.cpp render/performance_overlay.ui
|
render/performance_overlay.h render/performance_overlay.cpp render/performance_overlay.ui
|
||||||
libqt_common.h libqt_common.cpp
|
libqt_common.h libqt_common.cpp
|
||||||
|
updater/update_dialog.h updater/update_dialog.cpp updater/update_dialog.ui
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden")
|
set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden")
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,6 @@ void ConfigurePerGameAddons::LoadConfiguration() {
|
||||||
if (is_external_update) {
|
if (is_external_update) {
|
||||||
first_item->setData(static_cast<quint32>(patch.numeric_version), NUMERIC_VERSION);
|
first_item->setData(static_cast<quint32>(patch.numeric_version), NUMERIC_VERSION);
|
||||||
} else if (is_mod) {
|
} else if (is_mod) {
|
||||||
// qDebug() << patch.location;
|
|
||||||
first_item->setData(QString::fromStdString(patch.location), PATCH_LOCATION);
|
first_item->setData(QString::fromStdString(patch.location), PATCH_LOCATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
#include "frontend_common/settings_generator.h"
|
#include "frontend_common/settings_generator.h"
|
||||||
#include "qt_common/qt_string_lookup.h"
|
#include "qt_common/qt_string_lookup.h"
|
||||||
#include "render/performance_overlay.h"
|
#include "render/performance_overlay.h"
|
||||||
|
#include "updater/update_dialog.h"
|
||||||
#if defined(QT_STATICPLUGIN) && !defined(__APPLE__)
|
#if defined(QT_STATICPLUGIN) && !defined(__APPLE__)
|
||||||
#undef VMA_IMPLEMENTATION
|
#undef VMA_IMPLEMENTATION
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -539,7 +540,7 @@ MainWindow::MainWindow(bool has_broken_vulkan)
|
||||||
#ifdef ENABLE_UPDATE_CHECKER
|
#ifdef ENABLE_UPDATE_CHECKER
|
||||||
if (UISettings::values.check_for_updates) {
|
if (UISettings::values.check_for_updates) {
|
||||||
update_future = QtConcurrent::run(
|
update_future = QtConcurrent::run(
|
||||||
[]() -> std::optional<UpdateChecker::Update> { return UpdateChecker::GetUpdate(); });
|
[]() -> std::optional<Common::Net::Release> { return UpdateChecker::GetUpdate(); });
|
||||||
update_watcher.connect(&update_watcher, &QFutureWatcher<QString>::finished, this,
|
update_watcher.connect(&update_watcher, &QFutureWatcher<QString>::finished, this,
|
||||||
&MainWindow::OnEmulatorUpdateAvailable);
|
&MainWindow::OnEmulatorUpdateAvailable);
|
||||||
update_watcher.setFuture(update_future);
|
update_watcher.setFuture(update_future);
|
||||||
|
|
@ -4218,23 +4219,12 @@ void MainWindow::OnCaptureScreenshot() {
|
||||||
|
|
||||||
#ifdef ENABLE_UPDATE_CHECKER
|
#ifdef ENABLE_UPDATE_CHECKER
|
||||||
void MainWindow::OnEmulatorUpdateAvailable() {
|
void MainWindow::OnEmulatorUpdateAvailable() {
|
||||||
std::optional<UpdateChecker::Update> version = update_future.result();
|
std::optional<Common::Net::Release> version = update_future.result();
|
||||||
if (!version)
|
if (!version)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
QMessageBox update_prompt(this);
|
UpdateDialog dialog(version.value(), this);
|
||||||
update_prompt.setWindowTitle(tr("Update Available"));
|
dialog.exec();
|
||||||
update_prompt.setIcon(QMessageBox::Information);
|
|
||||||
update_prompt.addButton(QMessageBox::Yes);
|
|
||||||
update_prompt.addButton(QMessageBox::Ignore);
|
|
||||||
update_prompt.setText(tr("Download %1?").arg(QString::fromStdString(version->name)));
|
|
||||||
update_prompt.exec();
|
|
||||||
if (update_prompt.button(QMessageBox::Yes) == update_prompt.clickedButton()) {
|
|
||||||
auto const full_url =
|
|
||||||
fmt::format("{}/{}/releases/tag/", std::string{Common::g_build_auto_update_website},
|
|
||||||
std::string{Common::g_build_auto_update_repo});
|
|
||||||
QDesktopServices::openUrl(QUrl(QString::fromStdString(full_url + version->tag)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -493,8 +493,8 @@ private:
|
||||||
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
|
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
|
||||||
|
|
||||||
#ifdef ENABLE_UPDATE_CHECKER
|
#ifdef ENABLE_UPDATE_CHECKER
|
||||||
QFuture<std::optional<UpdateChecker::Update>> update_future;
|
QFuture<std::optional<Common::Net::Release>> update_future;
|
||||||
QFutureWatcher<std::optional<UpdateChecker::Update>> update_watcher;
|
QFutureWatcher<std::optional<Common::Net::Release>> update_watcher;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
MultiplayerState* multiplayer_state = nullptr;
|
MultiplayerState* multiplayer_state = nullptr;
|
||||||
|
|
|
||||||
178
src/yuzu/updater/update_dialog.cpp
Normal file
178
src/yuzu/updater/update_dialog.cpp
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include <QRadioButton>
|
||||||
|
#include <QSaveFile>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <qdesktopservices.h>
|
||||||
|
#include "common/logging.h"
|
||||||
|
#include "qt_common/abstract/frontend.h"
|
||||||
|
#include "qt_common/abstract/progress.h"
|
||||||
|
#include "ui_update_dialog.h"
|
||||||
|
#include "update_dialog.h"
|
||||||
|
|
||||||
|
#include "common/httplib.h"
|
||||||
|
|
||||||
|
#ifdef YUZU_BUNDLED_OPENSSL
|
||||||
|
#include <openssl/cert.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <QDesktopServices>
|
||||||
|
|
||||||
|
#undef GetSaveFileName
|
||||||
|
|
||||||
|
UpdateDialog::UpdateDialog(const Common::Net::Release& release, QWidget* parent)
|
||||||
|
: QDialog(parent), ui(new Ui::UpdateDialog) {
|
||||||
|
ui->setupUi(this);
|
||||||
|
|
||||||
|
ui->version->setText(
|
||||||
|
tr("%1 is available for download.").arg(QString::fromStdString(release.title)));
|
||||||
|
ui->url->setText(
|
||||||
|
tr("<a href=\"%1\">View on Forgejo</a>").arg(QString::fromStdString(release.html_url)));
|
||||||
|
|
||||||
|
std::string text{release.body};
|
||||||
|
if (auto pos = text.find("# Packages"); pos != std::string::npos) {
|
||||||
|
text = text.substr(0, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->body->setMarkdown(QString::fromStdString(text));
|
||||||
|
|
||||||
|
// TODO(crueter): Find a way to set default
|
||||||
|
const auto assets = release.GetPlatformAssets();
|
||||||
|
|
||||||
|
if (assets.empty()) {
|
||||||
|
ui->groupBox->setHidden(true);
|
||||||
|
connect(this, &QDialog::accepted, this, [release]() {
|
||||||
|
QDesktopServices::openUrl(QUrl{QString::fromStdString(release.html_url)});
|
||||||
|
});
|
||||||
|
} else if (assets.size() == 1) {
|
||||||
|
m_asset = assets[0];
|
||||||
|
|
||||||
|
connect(this, &QDialog::accepted, this, &UpdateDialog::Download);
|
||||||
|
} else {
|
||||||
|
u32 i = 0;
|
||||||
|
for (const Common::Net::Asset& a : assets) {
|
||||||
|
QRadioButton* r = new QRadioButton(tr(a.name.c_str()), this);
|
||||||
|
connect(r, &QRadioButton::toggled, this, [a, this](bool checked) {
|
||||||
|
if (checked)
|
||||||
|
m_asset = a;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i == 0)
|
||||||
|
r->setChecked(true);
|
||||||
|
++i;
|
||||||
|
|
||||||
|
ui->radioButtons->addWidget(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(this, &QDialog::accepted, this, &UpdateDialog::Download);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateDialog::~UpdateDialog() {
|
||||||
|
delete ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateDialog::Download() {
|
||||||
|
const auto filename = QtCommon::Frontend::GetSaveFileName(
|
||||||
|
tr("New Version Location"),
|
||||||
|
qApp->applicationDirPath() % QStringLiteral("/") % QString::fromStdString(m_asset.filename),
|
||||||
|
tr("All Files (*.*)"));
|
||||||
|
|
||||||
|
if (filename.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QSaveFile file(filename);
|
||||||
|
if (!file.open(QIODevice::Truncate | QIODevice::WriteOnly)) {
|
||||||
|
LOG_WARNING(Frontend, "Could not open file {}", filename.toStdString());
|
||||||
|
QtCommon::Frontend::Critical(tr("Failed to save file"),
|
||||||
|
tr("Could not open file %1 for writing.").arg(filename));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(crueter): Move to net.cpp
|
||||||
|
constexpr std::size_t timeout_seconds = 15;
|
||||||
|
|
||||||
|
std::unique_ptr<httplib::Client> client = std::make_unique<httplib::Client>(m_asset.url);
|
||||||
|
client->set_connection_timeout(timeout_seconds);
|
||||||
|
client->set_read_timeout(timeout_seconds);
|
||||||
|
client->set_write_timeout(timeout_seconds);
|
||||||
|
|
||||||
|
#ifdef YUZU_BUNDLED_OPENSSL
|
||||||
|
client->load_ca_cert_store(kCert, sizeof(kCert));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (client == nullptr) {
|
||||||
|
LOG_ERROR(Frontend, "Invalid URL {}{}", m_asset.url, m_asset.path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto progress =
|
||||||
|
QtCommon::Frontend::newProgressDialog(tr("Downloading..."), tr("Cancel"), 0, 100);
|
||||||
|
progress->show();
|
||||||
|
|
||||||
|
QGuiApplication::processEvents();
|
||||||
|
|
||||||
|
// Progress dialog.
|
||||||
|
auto progress_callback = [&](size_t processed_size, size_t total_size) {
|
||||||
|
QGuiApplication::processEvents();
|
||||||
|
progress->setValue(static_cast<int>((processed_size * 100) / total_size));
|
||||||
|
return !progress->wasCanceled();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write file in chunks.
|
||||||
|
auto content_receiver = [&file, filename](const char* t_data, size_t data_length) -> bool {
|
||||||
|
if (file.write(t_data, data_length) == -1) {
|
||||||
|
LOG_WARNING(Frontend, "Could not write {} bytes to file {}", data_length,
|
||||||
|
filename.toStdString());
|
||||||
|
QtCommon::Frontend::Critical(tr("Failed to save file"),
|
||||||
|
tr("Could not write to file %1.").arg(filename));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now send off request
|
||||||
|
auto result = client->Get(m_asset.path, content_receiver, progress_callback);
|
||||||
|
progress->close();
|
||||||
|
|
||||||
|
// commit to file
|
||||||
|
if (!file.commit()) {
|
||||||
|
LOG_WARNING(Frontend, "Could not commit to file {}", filename.toStdString());
|
||||||
|
QtCommon::Frontend::Critical(tr("Failed to save file"),
|
||||||
|
tr("Could not commit to file %1.").arg(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
LOG_ERROR(Frontend, "GET to {}{} returned null", m_asset.url, m_asset.path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& response = result.value();
|
||||||
|
if (response.status >= 400) {
|
||||||
|
LOG_ERROR(Frontend, "GET to {}{} returned error status code: {}", m_asset.url, m_asset.path,
|
||||||
|
response.status);
|
||||||
|
QtCommon::Frontend::Critical(tr("Failed to download file"),
|
||||||
|
tr("Could not download from %1%2\nError code: %3")
|
||||||
|
.arg(QString::fromStdString(m_asset.url),
|
||||||
|
QString::fromStdString(m_asset.path),
|
||||||
|
QString::number(response.status)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!response.headers.contains("content-type")) {
|
||||||
|
LOG_ERROR(Frontend, "GET to {}{} returned no content", m_asset.url, m_asset.path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download is complete. User may choose to open in the file manager.
|
||||||
|
auto button =
|
||||||
|
QtCommon::Frontend::Question(tr("Download Complete"),
|
||||||
|
tr("Successfully downloaded %1. Would you like to open it?")
|
||||||
|
.arg(QString::fromStdString(m_asset.filename)),
|
||||||
|
QtCommon::Frontend::Yes | QtCommon::Frontend::No);
|
||||||
|
|
||||||
|
if (button == QtCommon::Frontend::Yes) {
|
||||||
|
QDesktopServices::openUrl(QUrl::fromLocalFile(filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/yuzu/updater/update_dialog.h
Normal file
28
src/yuzu/updater/update_dialog.h
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include "common/net/net.h"
|
||||||
|
|
||||||
|
class QRadioButton;
|
||||||
|
namespace Ui {
|
||||||
|
class UpdateDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdateDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit UpdateDialog(const Common::Net::Release &release, QWidget* parent = nullptr);
|
||||||
|
~UpdateDialog();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void Download();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ui::UpdateDialog* ui;
|
||||||
|
QList<QRadioButton *> m_buttons;
|
||||||
|
Common::Net::Asset m_asset;
|
||||||
|
};
|
||||||
112
src/yuzu/updater/update_dialog.ui
Normal file
112
src/yuzu/updater/update_dialog.ui
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>UpdateDialog</class>
|
||||||
|
<widget class="QDialog" name="UpdateDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>655</width>
|
||||||
|
<height>551</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Update Available</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QLabel" name="url">
|
||||||
|
<property name="text">
|
||||||
|
<string><a href="%1">View on Forgejo</a></string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>Would you like to install this update?</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0" colspan="2">
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::StandardButton::No|QDialogButtonBox::StandardButton::Yes</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<widget class="QGroupBox" name="groupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Available Versions</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="radioButtons"/>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="version">
|
||||||
|
<property name="text">
|
||||||
|
<string>%1 is available for download.</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QTextBrowser" name="body">
|
||||||
|
<property name="horizontalScrollBarPolicy">
|
||||||
|
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
|
||||||
|
</property>
|
||||||
|
<property name="readOnly">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>UpdateDialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>UpdateDialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
||||||
Loading…
Add table
Reference in a new issue