From ca1727c08c3457f0f5abfe8856828b7f92340324 Mon Sep 17 00:00:00 2001 From: p2r3 Date: Thu, 5 Dec 2024 00:19:48 +0200 Subject: [PATCH] implement automatic updates --- CMakeLists.txt | 1 + globals.cpp | 3 ++ globals.h | 1 + main.cpp | 5 ++ tools/update.cpp | 129 +++++++++++++++++++++++++++++++++++++++++++++++ tools/update.h | 9 ++++ 6 files changed, 148 insertions(+) create mode 100644 tools/update.cpp create mode 100644 tools/update.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e3b6e4a..584cd17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,7 @@ add_executable(SppliceCPP main.cpp globals.cpp tools/curl.cpp + tools/update.cpp tools/qt.cpp tools/install.cpp tools/package.cpp diff --git a/globals.cpp b/globals.cpp index 8318b80..101cfba 100644 --- a/globals.cpp +++ b/globals.cpp @@ -29,3 +29,6 @@ const std::filesystem::path REPO_PATH = APP_DIR / "repositories.txt"; int SPPLICE_INSTALL_STATE = 0; // 0 - idle; 1 - installing; 2 - installed // TCP communication port between Portal 2 and Spplice (set during runtime) int SPPLICE_NETCON_PORT = -1; + +// Holds the current version's GitHub tag for automatic updates +const std::string SPPLICE_VERSION_TAG = "v0.6.3-alpha"; diff --git a/globals.h b/globals.h index 18c148a..8239a57 100644 --- a/globals.h +++ b/globals.h @@ -15,5 +15,6 @@ extern std::ofstream LOGFILE; extern const std::filesystem::path REPO_PATH; extern int SPPLICE_INSTALL_STATE; extern int SPPLICE_NETCON_PORT; +extern const std::string SPPLICE_VERSION_TAG; #endif diff --git a/main.cpp b/main.cpp index c4c160e..da2c08f 100644 --- a/main.cpp +++ b/main.cpp @@ -36,6 +36,7 @@ #include "tools/install.h" #include "tools/package.h" #include "tools/repo.h" +#include "tools/update.h" // Fetch and display packages from the given repository URL asynchronously void displayRepository (const std::string &url, const std::string &last, QVBoxLayout *container) { @@ -162,6 +163,7 @@ int main (int argc, char *argv[]) { } // Open the log file LOGFILE = std::ofstream(APP_DIR / "log.txt"); + LOGFILE << "Spplice " << SPPLICE_VERSION_TAG << std::endl; // Check for a CACHE_DIR override in cache_dir.txt checkCacheOverride(APP_DIR / "cache_dir.txt"); @@ -193,6 +195,9 @@ int main (int argc, char *argv[]) { // Initialize CURL ToolsCURL::init(); + // Check for updates on a separate thread + std::thread(ToolsUpdate::installUpdate).detach(); + QVBoxLayout *packageContainer = windowUI.PackageListLayout; // Connect the "Settings" button diff --git a/tools/update.cpp b/tools/update.cpp new file mode 100644 index 0000000..0fa7f81 --- /dev/null +++ b/tools/update.cpp @@ -0,0 +1,129 @@ +#include +#include +#include +#include +#include + +#ifdef TARGET_WINDOWS + #include +#else + #include + #include +#endif + +#include "curl.h" // ToolsCURL +#include "../globals.h" // Project globals + +// Definitions for this source file +#include "update.h" + +#ifndef TARGET_WINDOWS + const std::string updateBinary = "SppliceCPP"; +#else + const std::string updateBinary = "autoupdate"; +#endif + +std::filesystem::path getExecutablePath () { + +#ifdef TARGET_WINDOWS + wchar_t buffer[MAX_PATH]; + DWORD size = GetModuleFileNameW(NULL, buffer, MAX_PATH); + if (size == 0 || size == MAX_PATH) return std::filesystem::path(); +#else + char buffer[PATH_MAX]; + ssize_t size = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1); + if (size == -1) return std::filesystem::path(); + buffer[size] = '\0'; +#endif + + return std::filesystem::path(buffer); + +} + +void ToolsUpdate::installUpdate () { + + // Download the GitHub releases manifest containing one item + std::string releasesString = ToolsCURL::downloadString("https://api.github.com/repos/p2r3/spplice-cpp/releases?per_page=1"); + // If the download failed, do nothing + if (releasesString == "") { + LOGFILE << "[E] Failed to download GitHub releases list" << std::endl; + return; + } + + // Parse the JSON to get an array of releases + QJsonDocument doc = QJsonDocument::fromJson(QString::fromStdString(releasesString).toUtf8()); + QJsonArray releases = doc.array(); + // Get the latest (pre)release + QJsonObject latest = releases[0].toObject(); + + // If we're up to date, do nothing + const std::string latestTag = latest["tag_name"].toString().toStdString(); + if (SPPLICE_VERSION_TAG == latestTag) return; + LOGFILE << "[I] Update required (" << SPPLICE_VERSION_TAG << " -> " << latestTag << ")" << std::endl; + + // Parse the assets array + QJsonArray assets = latest["assets"].toArray(); + // Find the right binary for this platform + QJsonObject asset; + bool foundUpdateBinary = false; + for (const QJsonValue &curr : assets) { + asset = curr.toObject(); + if (asset["name"].toString().toStdString() == updateBinary) { + foundUpdateBinary = true; + break; + } + } + // If no matching update binary was found, request a manual update + if (!foundUpdateBinary) { + QMessageBox::information(nullptr, "Spplice Update", + "This version of Spplice is obsolete and requires a manual update.\nGo to p2r3.com/spplice to download the latest version."); + return; + } + + // Download the binary + const std::string url = asset["browser_download_url"].toString().toStdString(); + LOGFILE << "[I] Downloading update from \"" << url << '"' << std::endl; + + const std::filesystem::path updatePath = CACHE_DIR / "update_binary"; + bool success = ToolsCURL::downloadFile(url, updatePath); + // If the download failed, do nothing + if (!success) { + LOGFILE << "[E] Failed to download update from \"" << url << '"' << std::endl; + return; + } + + // Get the path of the currently running executable + const std::filesystem::path executablePath = getExecutablePath(); + // If this failed, do nothing + if (executablePath.empty()) { + LOGFILE << "[E] Failed to find own executable." << std::endl; + return; + } + LOGFILE << "[I] Found own executable at " << executablePath << std::endl; + + // Swap the running executable with the one we just downloaded +#ifdef TARGET_WINDOWS + // If on Windows, defer replacing until next reboot to avoid conflicts + if (MoveFileExW(updatePath.c_str(), executablePath.c_str(), MOVEFILE_DELAY_UNTIL_REBOOT)) { + LOGFILE << "[I] Scheduled update file replacement for next reboot" << std::endl; + } else { + LOGFILE << "[E] Failed to schedule update file replacement. ERRNO: " << GetLastError() << std::endl; + return; + } + QMessageBox::information(nullptr, "Spplice Update", + "Spplice has been updated.\nChanges will take effect after a system reboot."); +#else + // If on Linux, replace the executable right away + try { + std::filesystem::remove(executablePath); + std::filesystem::rename(updatePath, executablePath); + LOGFILE << "[I] Replaced " << executablePath << " with " << updatePath << std::endl; + } catch (const std::filesystem::filesystem_error& e) { + LOGFILE << "[E] Failed to replace executable post-update: " << e.what() << std::endl; + return; + } + QMessageBox::information(nullptr, "Spplice Update", + "Spplice has been updated.\nRestart Spplice to apply the changes."); +#endif + +} diff --git a/tools/update.h b/tools/update.h new file mode 100644 index 0000000..b727230 --- /dev/null +++ b/tools/update.h @@ -0,0 +1,9 @@ +#ifndef TOOLS_UPDATE_H +#define TOOLS_UPDATE_H + +class ToolsUpdate { + public: + static void installUpdate (); +}; + +#endif