mirror of
https://github.com/xenia-project/xenia.git
synced 2025-12-06 07:12:03 +01:00
Merge 5ca4d146e7 into 01ae24e46e
This commit is contained in:
commit
ffc94872da
|
|
@ -612,6 +612,10 @@ bool EmulatorWindow::Initialize() {
|
|||
"..." XE_BUILD_BRANCH);
|
||||
}));
|
||||
help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||||
help_menu->AddChild(
|
||||
MenuItem::Create(MenuItem::Type::kString, "Check for &updates...",
|
||||
std::bind(&EmulatorWindow::CheckForUpdates, this)));
|
||||
help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||||
help_menu->AddChild(MenuItem::Create(
|
||||
MenuItem::Type::kString, "&About...",
|
||||
[this]() { LaunchWebBrowser("https://xenia.jp/about/"); }));
|
||||
|
|
@ -1021,5 +1025,196 @@ void EmulatorWindow::SetInitializingShaderStorage(bool initializing) {
|
|||
UpdateTitle();
|
||||
}
|
||||
|
||||
class EmulatorWindow::UpdateDialog : public xe::ui::ImGuiDialog {
|
||||
public:
|
||||
UpdateDialog(xe::ui::ImGuiDrawer* imgui_drawer,
|
||||
xe::ui::UpdateManager* update_manager)
|
||||
: ui::ImGuiDialog(imgui_drawer),
|
||||
update_manager_(update_manager),
|
||||
state_(State::kChecking),
|
||||
download_progress_(0.0f) {}
|
||||
|
||||
void OnDraw(ImGuiIO& io) override {
|
||||
// Apply Xenia color scheme to the UpdateDialog window
|
||||
ImGui::PushStyleColor(
|
||||
ImGuiCol_WindowBg,
|
||||
ImVec4(0.16f, 0.16f, 0.16f, 1.0f));
|
||||
ImGui::PushStyleColor(
|
||||
ImGuiCol_TitleBgActive,
|
||||
ImVec4(0.3f, 0.5f, 0.9f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Button,
|
||||
ImVec4(0.25f, 0.45f, 0.85f, 1.0f));
|
||||
ImGui::PushStyleColor(
|
||||
ImGuiCol_ButtonHovered,
|
||||
ImVec4(0.35f, 0.55f, 0.95f, 1.0f));
|
||||
ImGui::PushStyleColor(
|
||||
ImGuiCol_ButtonActive,
|
||||
ImVec4(0.2f, 0.4f, 0.75f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border,
|
||||
ImVec4(0.3f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_BorderShadow,
|
||||
ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
ImGui::PushStyleColor(
|
||||
ImGuiCol_FrameBg,
|
||||
ImVec4(0.2f, 0.2f, 0.2f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered,
|
||||
ImVec4(0.25f, 0.25f, 0.25f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBgActive,
|
||||
ImVec4(0.3f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::PushStyleColor(
|
||||
ImGuiCol_Separator,
|
||||
ImVec4(0.4f, 0.4f, 0.4f, 1.0f));
|
||||
ImGui::PushStyleColor(
|
||||
ImGuiCol_PlotHistogram,
|
||||
ImVec4(0.25f, 0.45f, 0.85f, 1.0f));
|
||||
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f),
|
||||
ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(450, 220), ImGuiCond_FirstUseEver);
|
||||
|
||||
bool dialog_open = true;
|
||||
if (!ImGui::Begin(
|
||||
"Check for Updates", &dialog_open,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
|
||||
ImGui::PopStyleColor(13);
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::TextWrapped("Current build: %s@%s", XE_BUILD_BRANCH,
|
||||
XE_BUILD_COMMIT_SHORT);
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
switch (state_) {
|
||||
case State::kChecking:
|
||||
ImGui::TextWrapped("Checking for updates...");
|
||||
ImGui::ProgressBar(static_cast<float>(-1.0 * ImGui::GetTime()),
|
||||
ImVec2(-1, 0));
|
||||
break;
|
||||
|
||||
case State::kNoUpdate:
|
||||
ImGui::TextWrapped("You are running the latest version.");
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("OK", ImVec2(-1, 0))) {
|
||||
dialog_open = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::kUpdateAvailable:
|
||||
ImGui::TextWrapped("A new version is available: %s",
|
||||
update_info_.version.c_str());
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Download and Install", ImVec2(-1, 0))) {
|
||||
state_ = State::kDownloading;
|
||||
StartDownload();
|
||||
}
|
||||
if (ImGui::Button("Later", ImVec2(-1, 0))) {
|
||||
dialog_open = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::kDownloading:
|
||||
ImGui::TextWrapped("Downloading update...");
|
||||
ImGui::ProgressBar(download_progress_, ImVec2(-1, 0));
|
||||
ImGui::TextWrapped("%.1f MB / %.1f MB", downloaded_mb_, total_size_mb_);
|
||||
break;
|
||||
|
||||
case State::kInstalling:
|
||||
ImGui::TextWrapped("Installing update...");
|
||||
ImGui::TextWrapped("Xenia will restart momentarily.");
|
||||
break;
|
||||
|
||||
case State::kError:
|
||||
ImGui::TextWrapped("Failed to check for updates.");
|
||||
ImGui::TextWrapped("Please check your internet connection.");
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("OK", ImVec2(-1, 0))) {
|
||||
dialog_open = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
ImGui::PopStyleColor(13);
|
||||
|
||||
if (!dialog_open) {
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
void SetUpdateInfo(const xe::ui::UpdateInfo& info) {
|
||||
update_info_ = info;
|
||||
if (info.update_available) {
|
||||
state_ = State::kUpdateAvailable;
|
||||
} else {
|
||||
state_ = State::kNoUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
void SetError() { state_ = State::kError; }
|
||||
|
||||
private:
|
||||
enum class State {
|
||||
kChecking,
|
||||
kNoUpdate,
|
||||
kUpdateAvailable,
|
||||
kDownloading,
|
||||
kInstalling,
|
||||
kError
|
||||
};
|
||||
|
||||
void StartDownload() {
|
||||
update_manager_->DownloadAndInstallUpdate(
|
||||
update_info_.download_url,
|
||||
[this](uint64_t downloaded, uint64_t total) {
|
||||
if (total > 0) {
|
||||
download_progress_ =
|
||||
static_cast<float>(downloaded) / static_cast<float>(total);
|
||||
downloaded_mb_ = static_cast<float>(downloaded) / 1024.0f / 1024.0f;
|
||||
total_size_mb_ = static_cast<float>(total) / 1024.0f / 1024.0f;
|
||||
}
|
||||
},
|
||||
[this](bool success) {
|
||||
if (success) {
|
||||
state_ = State::kInstalling;
|
||||
} else {
|
||||
state_ = State::kError;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
xe::ui::UpdateManager* update_manager_;
|
||||
State state_;
|
||||
xe::ui::UpdateInfo update_info_;
|
||||
float download_progress_;
|
||||
float downloaded_mb_ = 0.0f;
|
||||
float total_size_mb_ = 0.0f;
|
||||
};
|
||||
|
||||
|
||||
void EmulatorWindow::CheckForUpdates() {
|
||||
if (!update_manager_) {
|
||||
update_manager_ = std::make_unique<xe::ui::UpdateManager>();
|
||||
}
|
||||
|
||||
auto dialog = new UpdateDialog(imgui_drawer_.get(), update_manager_.get());
|
||||
|
||||
// Start async update check
|
||||
update_manager_->CheckForUpdatesAsync(
|
||||
[dialog](const xe::ui::UpdateInfo& info) {
|
||||
if (info.version.empty()) {
|
||||
dialog->SetError();
|
||||
} else {
|
||||
dialog->SetUpdateInfo(info);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
#include "xenia/ui/window.h"
|
||||
#include "xenia/ui/window_listener.h"
|
||||
#include "xenia/ui/windowed_app_context.h"
|
||||
#include "xenia/ui/update_manager.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
namespace xe {
|
||||
|
|
@ -159,6 +160,12 @@ class EmulatorWindow {
|
|||
bool initializing_shader_storage_ = false;
|
||||
|
||||
std::unique_ptr<DisplayConfigDialog> display_config_dialog_;
|
||||
|
||||
class UpdateDialog;
|
||||
std::unique_ptr<xe::ui::UpdateManager> update_manager_;
|
||||
std::mutex update_dialog_mutex_;
|
||||
|
||||
void CheckForUpdates();
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
|
|
|||
|
|
@ -23,4 +23,5 @@ project("xenia-ui")
|
|||
links({
|
||||
"dwmapi",
|
||||
"dxgi",
|
||||
"winhttp",
|
||||
})
|
||||
|
|
|
|||
739
src/xenia/ui/update_manager.cc
Normal file
739
src/xenia/ui/update_manager.cc
Normal file
|
|
@ -0,0 +1,739 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2025. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/ui/update_manager.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "build/version.h"
|
||||
#include "xenia/base/filesystem.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/string_util.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
|
||||
#include <shellapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <winhttp.h>
|
||||
|
||||
#pragma comment(lib, "winhttp.lib")
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
namespace {
|
||||
|
||||
// GitHub API endpoint for latest release of Xenia
|
||||
const wchar_t* kGitHubApiHost = L"api.github.com";
|
||||
const wchar_t* kGitHubApiPath =
|
||||
L"/repos/xenia-project/release-builds-windows/releases/latest";
|
||||
|
||||
// Helper function for converting a wide string to UTF-8
|
||||
std::string WideToUtf8(const std::wstring& wide) {
|
||||
if (wide.empty()) return {};
|
||||
int size = WideCharToMultiByte(CP_UTF8, 0, wide.data(),
|
||||
static_cast<int>(wide.size()), nullptr, 0,
|
||||
nullptr, nullptr);
|
||||
std::string result(size, 0);
|
||||
WideCharToMultiByte(CP_UTF8, 0, wide.data(), static_cast<int>(wide.size()),
|
||||
result.data(), size, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper function for converting UTF-8 to wide string
|
||||
std::wstring Utf8ToWide(const std::string& utf8) {
|
||||
if (utf8.empty()) return {};
|
||||
int size = MultiByteToWideChar(CP_UTF8, 0, utf8.data(),
|
||||
static_cast<int>(utf8.size()), nullptr, 0);
|
||||
std::wstring result(size, 0);
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()),
|
||||
result.data(), size);
|
||||
return result;
|
||||
}
|
||||
|
||||
// JSON parser/extraction tool for the fields being queried
|
||||
std::string ExtractJsonString(const std::string& json, const std::string& key) {
|
||||
std::string search = "\"" + key + "\":\"";
|
||||
size_t pos = json.find(search);
|
||||
if (pos == std::string::npos) {
|
||||
XELOGE("Key '{}' not found in JSON", key);
|
||||
return {};
|
||||
}
|
||||
|
||||
pos += search.length();
|
||||
size_t end = json.find("\"", pos);
|
||||
if (end == std::string::npos) {
|
||||
XELOGE("Closing quote not found for key '{}'", key);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string value = json.substr(pos, end - pos);
|
||||
XELOGI("ExtractJsonString: {} = {}", key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UpdateManager::UpdateManager() = default;
|
||||
UpdateManager::~UpdateManager() = default;
|
||||
|
||||
// Use Xenia's build commit as the current version
|
||||
std::string UpdateManager::GetCurrentVersion() {
|
||||
|
||||
return XE_BUILD_COMMIT_SHORT;
|
||||
}
|
||||
|
||||
void UpdateManager::CheckForUpdatesAsync(
|
||||
std::function<void(const UpdateInfo&)> callback) {
|
||||
std::thread([this, callback]() {
|
||||
UpdateInfo info;
|
||||
info.update_available = false;
|
||||
|
||||
HINTERNET hSession = nullptr;
|
||||
HINTERNET hConnect = nullptr;
|
||||
HINTERNET hRequest = nullptr;
|
||||
|
||||
try {
|
||||
// Initialize WinHTTP
|
||||
hSession =
|
||||
WinHttpOpen(L"Xenia-Emulator/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
|
||||
if (!hSession) {
|
||||
XELOGE("WinHttpOpen failed: {}", GetLastError());
|
||||
callback(info);
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to GitHub API
|
||||
hConnect = WinHttpConnect(hSession, kGitHubApiHost,
|
||||
INTERNET_DEFAULT_HTTPS_PORT, 0);
|
||||
if (!hConnect) {
|
||||
XELOGE("WinHttpConnect failed: {}", GetLastError());
|
||||
callback(info);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create HTTPS request
|
||||
hRequest = WinHttpOpenRequest(
|
||||
hConnect, L"GET", kGitHubApiPath, nullptr, WINHTTP_NO_REFERER,
|
||||
WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);
|
||||
|
||||
if (!hRequest) {
|
||||
XELOGE("WinHttpOpenRequest failed: {}", GetLastError());
|
||||
callback(info);
|
||||
return;
|
||||
}
|
||||
// Disable SSL certificate validation
|
||||
DWORD security_flags = SECURITY_FLAG_IGNORE_UNKNOWN_CA |
|
||||
SECURITY_FLAG_IGNORE_CERT_DATE_INVALID |
|
||||
SECURITY_FLAG_IGNORE_CERT_CN_INVALID |
|
||||
SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE;
|
||||
WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &security_flags,
|
||||
sizeof(security_flags));
|
||||
|
||||
// Add User-Agent header
|
||||
std::wstring headers =
|
||||
L"User-Agent: Xenia-Emulator\r\n"
|
||||
L"Accept: application/vnd.github+json\r\n";
|
||||
WinHttpAddRequestHeaders(hRequest, headers.c_str(),
|
||||
static_cast<DWORD>(headers.length()),
|
||||
WINHTTP_ADDREQ_FLAG_ADD);
|
||||
|
||||
// Send request
|
||||
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
|
||||
WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) {
|
||||
XELOGE("WinHttpSendRequest failed: {}", GetLastError());
|
||||
callback(info);
|
||||
return;
|
||||
}
|
||||
|
||||
// Receive response
|
||||
if (!WinHttpReceiveResponse(hRequest, nullptr)) {
|
||||
XELOGE("WinHttpReceiveResponse failed: {}", GetLastError());
|
||||
callback(info);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read response data
|
||||
std::string response_data;
|
||||
DWORD bytes_available = 0;
|
||||
DWORD bytes_read = 0;
|
||||
|
||||
do {
|
||||
bytes_available = 0;
|
||||
if (!WinHttpQueryDataAvailable(hRequest, &bytes_available)) {
|
||||
XELOGE("WinHttpQueryDataAvailable failed: {}", GetLastError());
|
||||
break;
|
||||
}
|
||||
|
||||
if (bytes_available > 0) {
|
||||
std::vector<char> buffer(bytes_available + 1);
|
||||
if (WinHttpReadData(hRequest, buffer.data(), bytes_available,
|
||||
&bytes_read)) {
|
||||
response_data.append(buffer.data(), bytes_read);
|
||||
}
|
||||
}
|
||||
} while (bytes_available > 0);
|
||||
|
||||
// Parse response
|
||||
info = ParseReleaseInfo(response_data);
|
||||
std::string current_version = GetCurrentVersion();
|
||||
|
||||
if (!info.version.empty()) {
|
||||
if (response_data.find(current_version) != std::string::npos) {
|
||||
XELOGI("Already running latest version (commit: {})",
|
||||
current_version);
|
||||
info.update_available = false;
|
||||
} else if (info.version == current_version) {
|
||||
XELOGI("Already running latest version: {}", current_version);
|
||||
info.update_available = false;
|
||||
} else {
|
||||
XELOGI("Update available: {} (current: {})", info.version,
|
||||
current_version);
|
||||
info.update_available = true;
|
||||
}
|
||||
} else {
|
||||
XELOGI("No version information found in release");
|
||||
info.update_available = false;
|
||||
}
|
||||
// Catch any other exception not listed above
|
||||
} catch (...) {
|
||||
XELOGE("Exception during update check");
|
||||
}
|
||||
|
||||
// Cleanup for WinHTTP
|
||||
if (hRequest) WinHttpCloseHandle(hRequest);
|
||||
if (hConnect) WinHttpCloseHandle(hConnect);
|
||||
if (hSession) WinHttpCloseHandle(hSession);
|
||||
|
||||
// Call callback with result
|
||||
callback(info);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
UpdateInfo UpdateManager::ParseReleaseInfo(const std::string& json_data) {
|
||||
UpdateInfo info;
|
||||
|
||||
XELOGI("Parsing JSON response ({} bytes)", json_data.size());
|
||||
|
||||
// Extract tag_name as the version
|
||||
info.version = ExtractJsonString(json_data, "tag_name");
|
||||
if (info.version.empty()) {
|
||||
XELOGE("Failed to extract version from JSON");
|
||||
return info;
|
||||
}
|
||||
XELOGI("Version: {}", info.version);
|
||||
|
||||
// Find the assets array
|
||||
size_t assets_start = json_data.find("\"assets\":");
|
||||
if (assets_start == std::string::npos) {
|
||||
XELOGE("No 'assets' field found in JSON");
|
||||
return info;
|
||||
}
|
||||
XELOGI("Found assets field at position {}", assets_start);
|
||||
|
||||
// Find xenia_master.zip in the release assets (will be extracted for installation)
|
||||
size_t asset_name_pos =
|
||||
json_data.find("\"name\":\"xenia_master.zip\"", assets_start);
|
||||
|
||||
if (asset_name_pos == std::string::npos) {
|
||||
XELOGE("xenia_master.zip not found in assets");
|
||||
return info;
|
||||
}
|
||||
|
||||
XELOGI("Found xenia_master.zip at position {}", asset_name_pos);
|
||||
|
||||
// Find the asset object boundaries
|
||||
// Go backwards to find the opening brace of this asset object
|
||||
size_t asset_obj_start = json_data.rfind("{", asset_name_pos);
|
||||
if (asset_obj_start == std::string::npos || asset_obj_start < assets_start) {
|
||||
XELOGE("Could not find asset object start");
|
||||
return info;
|
||||
}
|
||||
|
||||
// Go forward to find the closing brace of this asset object
|
||||
// Count braces to handle nested objects
|
||||
int brace_count = 1;
|
||||
size_t asset_obj_end = asset_obj_start + 1;
|
||||
while (asset_obj_end < json_data.size() && brace_count > 0) {
|
||||
if (json_data[asset_obj_end] == '{') {
|
||||
brace_count++;
|
||||
} else if (json_data[asset_obj_end] == '}') {
|
||||
brace_count--;
|
||||
}
|
||||
asset_obj_end++;
|
||||
}
|
||||
|
||||
if (brace_count != 0) {
|
||||
XELOGE("Could not find asset object end");
|
||||
return info;
|
||||
}
|
||||
|
||||
XELOGI("Asset object spans from {} to {}", asset_obj_start, asset_obj_end);
|
||||
|
||||
// Extract the asset object substring
|
||||
std::string asset_obj =
|
||||
json_data.substr(asset_obj_start, asset_obj_end - asset_obj_start);
|
||||
|
||||
// Find browser_download_url within this object
|
||||
size_t url_field_pos = asset_obj.find("\"browser_download_url\":\"");
|
||||
if (url_field_pos == std::string::npos) {
|
||||
XELOGE("browser_download_url not found in asset object");
|
||||
XELOGI("Asset object: {}",
|
||||
asset_obj.substr(0, std::min(size_t(500), asset_obj.size())));
|
||||
return info;
|
||||
}
|
||||
|
||||
// Extract the URL value
|
||||
size_t url_start = url_field_pos + 24;
|
||||
size_t url_end = asset_obj.find("\"", url_start);
|
||||
|
||||
if (url_end == std::string::npos) {
|
||||
XELOGE("Could not find end of URL");
|
||||
return info;
|
||||
}
|
||||
|
||||
info.download_url = asset_obj.substr(url_start, url_end - url_start);
|
||||
XELOGI("Extracted download URL: {}", info.download_url);
|
||||
|
||||
// Validate the URL
|
||||
if (info.download_url.empty() ||
|
||||
info.download_url.find("http") == std::string::npos) {
|
||||
XELOGE("Invalid download URL: {}", info.download_url);
|
||||
info.download_url.clear();
|
||||
return info;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
void UpdateManager::DownloadAndInstallUpdate(
|
||||
const std::string& download_url,
|
||||
std::function<void(uint64_t, uint64_t)> progress_callback,
|
||||
std::function<void(bool)> completion_callback) {
|
||||
std::thread([this, download_url, progress_callback, completion_callback]() {
|
||||
try {
|
||||
XELOGI("Starting download and install for URL: {}", download_url);
|
||||
|
||||
// Download the file to zip_data
|
||||
auto zip_data = DownloadFile(download_url, progress_callback);
|
||||
|
||||
XELOGI("Download completed. Received {} bytes", zip_data.size());
|
||||
|
||||
if (zip_data.empty()) {
|
||||
XELOGE("Failed to download update - no data received");
|
||||
completion_callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract and replace files
|
||||
XELOGI("Starting extraction...");
|
||||
bool success = ExtractAndReplace(zip_data);
|
||||
|
||||
if (success) {
|
||||
XELOGI("Update installed successfully");
|
||||
completion_callback(true);
|
||||
|
||||
// Restart Xenia after 2 seconds elapsed
|
||||
XELOGI("Restarting in 2 seconds...");
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
RestartApplication();
|
||||
} else {
|
||||
XELOGE("Failed to install update - extraction failed");
|
||||
completion_callback(false);
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
XELOGE("Exception during update installation: {}", e.what());
|
||||
completion_callback(false);
|
||||
} catch (...) {
|
||||
XELOGE("Unknown exception during update installation");
|
||||
completion_callback(false);
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> UpdateManager::DownloadFile(
|
||||
const std::string& url,
|
||||
std::function<void(uint64_t, uint64_t)> progress_callback) {
|
||||
std::vector<uint8_t> file_data;
|
||||
|
||||
XELOGI("Attempting to download from URL: {}", url);
|
||||
|
||||
// Parse URL to extract host and path
|
||||
std::regex url_regex(R"(https?://([^/]+)(/.*)?)");
|
||||
std::smatch match;
|
||||
|
||||
if (!std::regex_match(url, match, url_regex) || match.size() < 2) {
|
||||
XELOGE("Invalid URL format: {}", url);
|
||||
|
||||
// Try alternative parsing for GitHub URLs
|
||||
size_t protocol_end = url.find("://");
|
||||
if (protocol_end == std::string::npos) {
|
||||
XELOGE("No protocol found in URL");
|
||||
return file_data;
|
||||
}
|
||||
|
||||
size_t host_start = protocol_end + 3;
|
||||
size_t path_start = url.find('/', host_start);
|
||||
|
||||
if (path_start == std::string::npos) {
|
||||
XELOGE("No path found in URL");
|
||||
return file_data;
|
||||
}
|
||||
|
||||
std::string host = url.substr(host_start, path_start - host_start);
|
||||
std::string path = url.substr(path_start);
|
||||
|
||||
XELOGI("Manually parsed - Host: {}, Path: {}", host, path);
|
||||
|
||||
return DownloadFileWithParsedUrl(Utf8ToWide(host), Utf8ToWide(path),
|
||||
progress_callback);
|
||||
}
|
||||
|
||||
std::wstring host = Utf8ToWide(match[1].str());
|
||||
std::wstring path = match.size() > 2 ? Utf8ToWide(match[2].str()) : L"/";
|
||||
|
||||
if (path.empty()) {
|
||||
path = L"/";
|
||||
}
|
||||
|
||||
XELOGI("Parsed URL - Host: {}, Path: {}", WideToUtf8(host), WideToUtf8(path));
|
||||
|
||||
return DownloadFileWithParsedUrl(host, path, progress_callback);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> UpdateManager::DownloadFileWithParsedUrl(
|
||||
const std::wstring& host, const std::wstring& path,
|
||||
std::function<void(uint64_t, uint64_t)> progress_callback) {
|
||||
std::vector<uint8_t> file_data;
|
||||
|
||||
XELOGI("Downloading from host: {}, path: {}", WideToUtf8(host),
|
||||
WideToUtf8(path));
|
||||
|
||||
HINTERNET hSession = nullptr;
|
||||
HINTERNET hConnect = nullptr;
|
||||
HINTERNET hRequest = nullptr;
|
||||
|
||||
try {
|
||||
hSession =
|
||||
WinHttpOpen(L"Xenia-Emulator/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
|
||||
if (!hSession) {
|
||||
XELOGE("WinHttpOpen failed: {}", GetLastError());
|
||||
return file_data;
|
||||
}
|
||||
|
||||
hConnect =
|
||||
WinHttpConnect(hSession, host.c_str(), INTERNET_DEFAULT_HTTPS_PORT, 0);
|
||||
if (!hConnect) {
|
||||
XELOGE("WinHttpConnect failed: {}", GetLastError());
|
||||
return file_data;
|
||||
}
|
||||
|
||||
hRequest = WinHttpOpenRequest(
|
||||
hConnect, L"GET", path.c_str(), nullptr, WINHTTP_NO_REFERER,
|
||||
WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);
|
||||
|
||||
if (!hRequest) {
|
||||
XELOGE("WinHttpOpenRequest failed: {}", GetLastError());
|
||||
return file_data;
|
||||
}
|
||||
|
||||
DWORD security_flags = SECURITY_FLAG_IGNORE_UNKNOWN_CA |
|
||||
SECURITY_FLAG_IGNORE_CERT_DATE_INVALID |
|
||||
SECURITY_FLAG_IGNORE_CERT_CN_INVALID |
|
||||
SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE;
|
||||
WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &security_flags,
|
||||
sizeof(security_flags));
|
||||
|
||||
DWORD redirect_policy = WINHTTP_OPTION_REDIRECT_POLICY_ALWAYS;
|
||||
WinHttpSetOption(hRequest, WINHTTP_OPTION_REDIRECT_POLICY, &redirect_policy,
|
||||
sizeof(redirect_policy));
|
||||
|
||||
XELOGI("Sending HTTP request...");
|
||||
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
|
||||
WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) {
|
||||
XELOGE("WinHttpSendRequest failed: {}", GetLastError());
|
||||
return file_data;
|
||||
}
|
||||
|
||||
XELOGI("Waiting for response...");
|
||||
if (!WinHttpReceiveResponse(hRequest, nullptr)) {
|
||||
XELOGE("WinHttpReceiveResponse failed: {}", GetLastError());
|
||||
return file_data;
|
||||
}
|
||||
|
||||
DWORD status_code = 0;
|
||||
DWORD size = sizeof(status_code);
|
||||
WinHttpQueryHeaders(hRequest,
|
||||
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
nullptr, &status_code, &size, nullptr);
|
||||
|
||||
XELOGI("HTTP Status: {}", status_code);
|
||||
|
||||
if (status_code != 200) {
|
||||
XELOGE("Unexpected HTTP status code: {}", status_code);
|
||||
return file_data;
|
||||
}
|
||||
|
||||
// Get content length (to be used for calculating download progress in UI and in logs)
|
||||
DWORD content_length = 0;
|
||||
size = sizeof(content_length);
|
||||
if (WinHttpQueryHeaders(
|
||||
hRequest, WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
nullptr, &content_length, &size, nullptr)) {
|
||||
XELOGI("Content length: {} bytes ({:.2f} MB)", content_length,
|
||||
content_length / 1024.0 / 1024.0);
|
||||
} else {
|
||||
XELOGI("Content length not provided by server");
|
||||
}
|
||||
|
||||
// Download with progress tracking
|
||||
DWORD bytes_available = 0;
|
||||
DWORD bytes_read = 0;
|
||||
DWORD total_downloaded = 0;
|
||||
|
||||
XELOGI("Starting download...");
|
||||
|
||||
do {
|
||||
bytes_available = 0;
|
||||
if (!WinHttpQueryDataAvailable(hRequest, &bytes_available)) {
|
||||
XELOGE("WinHttpQueryDataAvailable failed: {}", GetLastError());
|
||||
break;
|
||||
}
|
||||
|
||||
if (bytes_available > 0) {
|
||||
std::vector<uint8_t> buffer(bytes_available);
|
||||
if (WinHttpReadData(hRequest, buffer.data(), bytes_available,
|
||||
&bytes_read)) {
|
||||
file_data.insert(file_data.end(), buffer.begin(),
|
||||
buffer.begin() + bytes_read);
|
||||
total_downloaded += bytes_read;
|
||||
|
||||
// Report progress with bytes downloaded
|
||||
if (progress_callback) {
|
||||
progress_callback(total_downloaded, content_length);
|
||||
}
|
||||
|
||||
if (total_downloaded % (5 * 1024 * 1024) < bytes_read) {
|
||||
XELOGI("Downloaded {:.2f} MB...",
|
||||
total_downloaded / 1024.0 / 1024.0);
|
||||
}
|
||||
} else {
|
||||
XELOGE("WinHttpReadData failed: {}", GetLastError());
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (bytes_available > 0);
|
||||
|
||||
XELOGI("Download complete: {} bytes ({:.2f} MB)", total_downloaded,
|
||||
total_downloaded / 1024.0 / 1024.0);
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
XELOGE("Exception during file download: {}", e.what());
|
||||
} catch (...) {
|
||||
XELOGE("Unknown exception during file download");
|
||||
}
|
||||
|
||||
if (hRequest) WinHttpCloseHandle(hRequest);
|
||||
if (hConnect) WinHttpCloseHandle(hConnect);
|
||||
if (hSession) WinHttpCloseHandle(hSession);
|
||||
|
||||
return file_data;
|
||||
}
|
||||
|
||||
bool UpdateManager::ExtractAndReplace(const std::vector<uint8_t>& zip_data) {
|
||||
XELOGI("ExtractAndReplace called with {} bytes", zip_data.size());
|
||||
|
||||
// Get temp path
|
||||
wchar_t temp_path[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, temp_path);
|
||||
|
||||
std::wstring zip_file = std::wstring(temp_path) + L"xenia_update.zip";
|
||||
|
||||
// Write .zip to temp file
|
||||
HANDLE hFile = CreateFileW(zip_file.c_str(), GENERIC_WRITE, 0, nullptr,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hFile == INVALID_HANDLE_VALUE) {
|
||||
XELOGE("Failed to create temp file: {}", GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD bytes_written = 0;
|
||||
if (!WriteFile(hFile, zip_data.data(), static_cast<DWORD>(zip_data.size()),
|
||||
&bytes_written, nullptr)) {
|
||||
XELOGE("Failed to write ZIP file: {}", GetLastError());
|
||||
CloseHandle(hFile);
|
||||
return false;
|
||||
}
|
||||
CloseHandle(hFile);
|
||||
XELOGI("Wrote {} bytes to ZIP file", bytes_written);
|
||||
|
||||
// Get paths
|
||||
std::wstring app_dir = xe::filesystem::GetExecutableFolder();
|
||||
std::wstring exe_path = xe::filesystem::GetExecutablePath();
|
||||
std::wstring temp_extract_dir =
|
||||
std::wstring(temp_path) + L"xenia_update_temp\\";
|
||||
|
||||
// Extract .zip via PowerShell
|
||||
std::wstring extract_cmd =
|
||||
L"PowerShell -WindowStyle Hidden -Command \"Expand-Archive -Path '" +
|
||||
zip_file + L"' -DestinationPath '" + temp_extract_dir + L"' -Force\"";
|
||||
|
||||
XELOGI("Extracting ZIP...");
|
||||
STARTUPINFOW si = {sizeof(si)};
|
||||
si.dwFlags = STARTF_USESHOWWINDOW;
|
||||
si.wShowWindow = SW_HIDE;
|
||||
PROCESS_INFORMATION pi = {};
|
||||
|
||||
if (!CreateProcessW(nullptr, const_cast<wchar_t*>(extract_cmd.c_str()),
|
||||
nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) {
|
||||
XELOGE("Failed to start extraction: {}", GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
WaitForSingleObject(pi.hProcess, 30000);
|
||||
DWORD exit_code;
|
||||
GetExitCodeProcess(pi.hProcess, &exit_code);
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
|
||||
if (exit_code != 0) {
|
||||
XELOGE("Extraction failed with exit code: {}", exit_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
XELOGI("Extraction complete");
|
||||
|
||||
// Create batch script updater for overwriting the contents of the Xenia folder
|
||||
std::wstring batch_file = std::wstring(temp_path) + L"xenia_update.bat";
|
||||
std::wstring source_dir = temp_extract_dir;
|
||||
|
||||
// Get process ID for waiting
|
||||
DWORD pid = GetCurrentProcessId();
|
||||
|
||||
// Batch script content
|
||||
std::string batch_content =
|
||||
"@echo off\r\n"
|
||||
"REM Wait for process to exit\r\n"
|
||||
":wait_loop\r\n"
|
||||
"tasklist /FI \"PID eq " +
|
||||
std::to_string(pid) + "\" 2>NUL | find \"" + std::to_string(pid) +
|
||||
"\" >NUL\r\n"
|
||||
"if \"%ERRORLEVEL%\"==\"0\" (\r\n"
|
||||
" timeout /t 1 /nobreak >nul\r\n"
|
||||
" goto wait_loop\r\n"
|
||||
")\r\n"
|
||||
"\r\n"
|
||||
"timeout /t 1 /nobreak >nul\r\n"
|
||||
"\r\n"
|
||||
"set SOURCE_DIR=" +
|
||||
WideToUtf8(source_dir) +
|
||||
"\r\n"
|
||||
"set DEST_DIR=" +
|
||||
WideToUtf8(app_dir) +
|
||||
"\r\n"
|
||||
"\r\n"
|
||||
"REM Backup old xenia.exe\r\n"
|
||||
"cd /d \"%DEST_DIR%\"\r\n"
|
||||
"if exist xenia.exe move /y xenia.exe xenia.exe.old >nul 2>&1\r\n"
|
||||
"\r\n"
|
||||
"REM Copy all new files\r\n"
|
||||
"xcopy /E /I /Y /R /Q \"%SOURCE_DIR%\\*\" \"%DEST_DIR%\\\" >nul 2>&1\r\n"
|
||||
"\r\n"
|
||||
"if errorlevel 1 (\r\n"
|
||||
" if exist xenia.exe.old move /y xenia.exe.old xenia.exe >nul 2>&1\r\n"
|
||||
" exit /b 1\r\n"
|
||||
")\r\n"
|
||||
"\r\n"
|
||||
"REM Delete backup if successful\r\n"
|
||||
"if exist xenia.exe.old del /q xenia.exe.old >nul 2>&1\r\n"
|
||||
"\r\n"
|
||||
"timeout /t 1 /nobreak >nul\r\n"
|
||||
"\r\n"
|
||||
"REM Restart Xenia\r\n"
|
||||
"start \"\" \"%DEST_DIR%\\xenia.exe\"\r\n"
|
||||
"\r\n"
|
||||
"REM Cleanup\r\n"
|
||||
"timeout /t 1 /nobreak >nul\r\n"
|
||||
"del /q \"" +
|
||||
WideToUtf8(zip_file) +
|
||||
"\" >nul 2>&1\r\n"
|
||||
"rd /s /q \"" +
|
||||
WideToUtf8(temp_extract_dir) +
|
||||
"\" >nul 2>&1\r\n"
|
||||
"\r\n"
|
||||
"REM Self-delete and close\r\n"
|
||||
"(goto) 2>nul & del \"%~f0\"\r\n";
|
||||
|
||||
// Write batch file
|
||||
HANDLE hBatch = CreateFileW(batch_file.c_str(), GENERIC_WRITE, 0, nullptr,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hBatch == INVALID_HANDLE_VALUE) {
|
||||
XELOGE("Failed to create batch file: {}", GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD batch_written = 0;
|
||||
WriteFile(hBatch, batch_content.c_str(),
|
||||
static_cast<DWORD>(batch_content.size()), &batch_written, nullptr);
|
||||
CloseHandle(hBatch);
|
||||
|
||||
XELOGI("Batch updater created: {} bytes", batch_written);
|
||||
XELOGI("Update staged successfully");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdateManager::RestartApplication() {
|
||||
wchar_t temp_path[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, temp_path);
|
||||
std::wstring batch_file = std::wstring(temp_path) + L"xenia_update.bat";
|
||||
|
||||
XELOGI("Launching batch updater: {}", WideToUtf8(batch_file));
|
||||
|
||||
// Launch batch script
|
||||
STARTUPINFOW si = {sizeof(si)};
|
||||
si.dwFlags = STARTF_USESHOWWINDOW;
|
||||
si.wShowWindow = SW_SHOW;
|
||||
PROCESS_INFORMATION pi = {};
|
||||
|
||||
std::wstring cmd = L"cmd.exe /c \"" + batch_file + L"\"";
|
||||
|
||||
if (CreateProcessW(nullptr, const_cast<wchar_t*>(cmd.c_str()), nullptr,
|
||||
nullptr, FALSE, CREATE_NEW_CONSOLE, nullptr, nullptr, &si,
|
||||
&pi)) {
|
||||
XELOGI("Updater launched successfully");
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
|
||||
// Give batch script time to start
|
||||
Sleep(500);
|
||||
|
||||
// Exit Xenia immediately
|
||||
XELOGI("Exiting Xenia for update...");
|
||||
ExitProcess(0);
|
||||
} else {
|
||||
XELOGE("Failed to launch updater: {}", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
59
src/xenia/ui/update_manager.h
Normal file
59
src/xenia/ui/update_manager.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2025. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_UI_UPDATE_MANAGER_H_
|
||||
#define XENIA_UI_UPDATE_MANAGER_H_
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
class ImGuiDrawer;
|
||||
|
||||
struct UpdateInfo {
|
||||
std::string version;
|
||||
std::string download_url;
|
||||
bool update_available;
|
||||
};
|
||||
|
||||
class UpdateManager {
|
||||
public:
|
||||
UpdateManager();
|
||||
~UpdateManager();
|
||||
|
||||
// Checks for updates asynchronously
|
||||
void CheckForUpdatesAsync(std::function<void(const UpdateInfo&)> callback);
|
||||
|
||||
// Downloads and installs an update
|
||||
void DownloadAndInstallUpdate(
|
||||
const std::string& download_url,
|
||||
std::function<void(uint64_t, uint64_t)> progress_callback,
|
||||
std::function<void(bool)> completion_callback);
|
||||
|
||||
private:
|
||||
std::string GetCurrentVersion();
|
||||
UpdateInfo ParseReleaseInfo(const std::string& json_data);
|
||||
std::vector<uint8_t> DownloadFile(
|
||||
const std::string& url,
|
||||
std::function<void(uint64_t, uint64_t)> progress_callback);
|
||||
std::vector<uint8_t> DownloadFileWithParsedUrl(
|
||||
const std::wstring& host, const std::wstring& path,
|
||||
std::function<void(uint64_t, uint64_t)> progress_callback);
|
||||
bool ExtractAndReplace(const std::vector<uint8_t>& zip_data);
|
||||
void RestartApplication();
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_UI_UPDATE_MANAGER_H_
|
||||
Loading…
Reference in a new issue