Ctrl K
ComunidadGuías…

server/src/update.cpp

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * @file src/update.cpp
 * @brief Definitions for update checking, notification, and command execution.
 */

#include "update.h"

// standard includes
#include <algorithm>
#include <cstdio>
#include <future>
#include <sstream>
#include <thread>
#include <variant>

// lib includes
#include <nlohmann/json.hpp>

// local includes
#include "config.h"
#include "httpcommon.h"
#include "logging.h"
#include "nvhttp.h"  // for save_state persistence hooks
#include "platform/common.h"
#include "process.h"
#include "rtsp.h"  // for session_count
#include "system_tray.h"
#include "utility.h"
#include "version_compare.h"

using namespace std::literals;

namespace update {
  state_t state;

  static size_t write_to_string(void *contents, size_t size, size_t nmemb, void *userp) {
    size_t total = size * nmemb;
    auto *out = static_cast<std::string *>(userp);
    out->append(static_cast<char *>(contents), total);
    return total;
  }

  bool download_github_release_data(const std::string &owner, const std::string &repo, std::string &out_json) {
    std::string url = "https://api.github.com/repos/" + owner + "/" + repo + "/releases";
    CURL *curl = curl_easy_init();
    if (!curl) {
      BOOST_LOG(error) << "CURL init failed for GitHub API"sv;
      return false;
    }
    struct curl_slist *headers = nullptr;
    headers = curl_slist_append(headers, "Accept: application/vnd.github+json");
    headers = curl_slist_append(headers, "X-GitHub-Api-Version: 2022-11-28");
    headers = curl_slist_append(headers, "User-Agent: Sunshine-Updater/1.0");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    bool tls_configured = http::configure_curl_tls(curl);
    if (!tls_configured) {
      BOOST_LOG(warning) << "GitHub release check is using libcurl defaults for TLS";
    }
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_string);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &out_json);
    char errbuf[CURL_ERROR_SIZE] = {0};
    curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
      BOOST_LOG(error) << "GitHub API request failed: "sv << curl_easy_strerror(res);
      if (errbuf[0] != '\0') {
        BOOST_LOG(error) << "GitHub API error detail: "sv << errbuf;
      }
      long verify_result = 0;
      if (curl_easy_getinfo(curl, CURLINFO_SSL_VERIFYRESULT, &verify_result) == CURLE_OK && verify_result != 0) {
        BOOST_LOG(error) << "TLS verification result code: "sv << verify_result;
      }
#ifdef CURLINFO_OS_ERRNO
      long os_err = 0;
      if (curl_easy_getinfo(curl, CURLINFO_OS_ERRNO, &os_err) == CURLE_OK && os_err != 0) {
        BOOST_LOG(error) << "OS-level error code: "sv << os_err;
      }
#endif
      curl_slist_free_all(headers);
      curl_easy_cleanup(curl);
      return false;
    }
    long code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    if (code < 200 || code >= 300) {
      BOOST_LOG(error) << "GitHub API returned HTTP "sv << code;
      return false;
    }
    return true;
  }

  // (removed) previous date-based comparison helper

  static void notify_new_version(const std::string &version, bool prerelease) {
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
    if (version.empty()) {
      return;
    }
    // Checks run on every stream start and on the periodic timer, so an update
    // that stays pending would re-post the same toast each time and stack
    // copies in the Action Center. Suppress duplicates for the lifetime of the
    // process: the earlier toast remains pending in the Action Center, and a
    // restart (which clears the Action Center's pending toasts) re-posts once.
    static std::string s_notified_version;
    static bool s_notified_prerelease = false;
    if (version == s_notified_version && prerelease == s_notified_prerelease) {
      BOOST_LOG(debug) << "Update notification suppressed: already notified for "sv << version;
      return;
    }
    std::string title = prerelease ? "New update available (Pre-release)" : "New update available (Stable)";
    std::string body = "Version " + version;
    state.last_notified_version = version;
    state.last_notified_is_prerelease = prerelease;
    state.last_notified_url = prerelease ? state.latest_prerelease.url : state.latest_release.url;
    // On click, open the release page directly
    system_tray::tray_notify(title.c_str(), body.c_str(), []() {
      open_last_notified_release_page();
    });
    s_notified_version = version;
    s_notified_prerelease = prerelease;
#endif
  }

  static void perform_check() {
    state.check_in_progress = true;
    auto fg = util::fail_guard([]() {
      state.check_in_progress = false;
    });
    try {
      const bool allow_prerelease_updates =
        config::sunshine.notify_pre_releases || version_compare::is_prerelease_channel(PROJECT_VERSION);
      // Fetch releases list once and compute latest stable/prerelease
      BOOST_LOG(info) << "Update check: querying GitHub releases from repo "sv
                      << SUNSHINE_REPO_OWNER << '/' << SUNSHINE_REPO_NAME;
      std::string releases_json;
      if (download_github_release_data(SUNSHINE_REPO_OWNER, SUNSHINE_REPO_NAME, releases_json)) {
        auto j = nlohmann::json::parse(releases_json);
        // Reset release info
        state.latest_release = release_info_t {};
        state.latest_prerelease = release_info_t {};

        // Track best by semver
        release_info_t best_stable;
        release_info_t best_pre;

        for (auto &rel : j) {
          const bool is_prerelease = rel.value("prerelease", false);
          const bool is_draft = rel.value("draft", false);
          if (is_draft) {
            continue;
          }

          // Parse assets for this release
          std::vector<asset_info_t> assets;
          if (rel.contains("assets") && rel["assets"].is_array()) {
            for (auto &asset : rel["assets"]) {
              asset_info_t ai;
              ai.name = asset.value("name", "");
              ai.download_url = asset.value("browser_download_url", "");
              ai.size = asset.value("size", 0);
              ai.content_type = asset.value("content_type", "");
              std::string digest = asset.value("digest", "");
              if (digest.starts_with("sha256:")) {
                ai.sha256 = digest.substr(7);
              }
              if (!ai.name.empty() && !ai.download_url.empty()) {
                assets.push_back(ai);
              }
            }
          }

          const std::string tag = rel.value("tag_name", "");
          if (!is_prerelease) {
            if (best_stable.version.empty() || version_compare::compare_semver(best_stable.version, tag) < 0) {
              best_stable.version = tag;
              best_stable.url = rel.value("html_url", "");
              best_stable.name = rel.value("name", "");
              best_stable.body = rel.value("body", "");
              best_stable.published_at = rel.value("published_at", "");
              best_stable.is_prerelease = false;
              best_stable.assets = assets;
            }
          } else if (allow_prerelease_updates) {
            if (best_pre.version.empty() || version_compare::compare_semver(best_pre.version, tag) < 0) {
              best_pre.version = tag;
              best_pre.url = rel.value("html_url", "");
              best_pre.name = rel.value("name", "");
              best_pre.body = rel.value("body", "");
              best_pre.published_at = rel.value("published_at", "");
              best_pre.is_prerelease = true;
              best_pre.assets = assets;
            }
          }
        }

        state.latest_release = best_stable;
        state.latest_prerelease = best_pre;
        if (!state.latest_release.version.empty()) {
          BOOST_LOG(info) << "Update check: latest stable tag="sv << state.latest_release.version;
        }
        if (!state.latest_prerelease.version.empty()) {
          BOOST_LOG(info) << "Update check: latest prerelease tag="sv << state.latest_prerelease.version;
        }
      }
      state.last_check_time = std::chrono::steady_clock::now();

      const std::string installed_version_tag = PROJECT_VERSION;
      const std::string latest_stable_tag = state.latest_release.version;
      const std::string latest_pre_tag = state.latest_prerelease.version;
      bool stable_available = !latest_stable_tag.empty() &&
                              (version_compare::compare_semver(installed_version_tag, latest_stable_tag) < 0);
      bool prerelease_available = allow_prerelease_updates &&
                                  !latest_pre_tag.empty() &&
                                  (version_compare::compare_semver(installed_version_tag, latest_pre_tag) < 0) &&
                                  (!latest_stable_tag.empty() ?
                                     (version_compare::compare_semver(latest_stable_tag, latest_pre_tag) < 0) :
                                     true);

      if (prerelease_available) {
        BOOST_LOG(info) << "Update check: prerelease available tag="sv << state.latest_prerelease.version
                        << ", installed="sv << installed_version_tag;
        notify_new_version(state.latest_prerelease.version, true);
      } else if (stable_available) {
        BOOST_LOG(info) << "Update check: stable available tag="sv << state.latest_release.version
                        << ", installed="sv << installed_version_tag;
        notify_new_version(state.latest_release.version, false);
      } else {
        BOOST_LOG(info) << "Update check (tag-based): up-to-date. installed="sv << installed_version_tag
                        << ", stable="sv << latest_stable_tag
                        << ", prerelease="sv << latest_pre_tag;
      }
    } catch (std::exception &e) {
      BOOST_LOG(warning) << "Update check failed: "sv << e.what();
    }
  }

  void trigger_check(bool force) {
    const bool in_progress = state.check_in_progress.load();
    if (in_progress) {
      BOOST_LOG(info) << "Update check trigger skipped: another check is in progress (force="sv << (force ? "true"sv : "false"sv) << ')';
      return;
    }
    if (!force && config::sunshine.update_check_interval_seconds == 0) {
      BOOST_LOG(info) << "Update check trigger skipped: checks are disabled by config (interval=0)"sv;
      return;
    }
    if (!force) {
      auto now = std::chrono::steady_clock::now();
      auto since_last = std::chrono::duration_cast<std::chrono::seconds>(now - state.last_check_time);
      if (since_last < std::chrono::seconds(config::sunshine.update_check_interval_seconds)) {
        BOOST_LOG(info) << "Update check trigger throttled: last ran "sv << since_last.count() << "s ago (interval="sv << config::sunshine.update_check_interval_seconds << 's' << ')';
        return;
      }
    }
    BOOST_LOG(info) << "Update check trigger accepted (force="sv << (force ? "true"sv : "false"sv) << ')';
    std::thread([]() {
      perform_check();
    }).detach();
  }

  // update command removed

  void on_stream_started() {
    // Kick a metadata refresh after a small delay but do not auto-execute updates while streaming.
    std::thread([]() {
      std::this_thread::sleep_for(3s);
      trigger_check(true);
    }).detach();
  }

  void periodic() {
    // Only trigger checks if no streaming sessions are active
    if (rtsp_stream::session_count() == 0) {
      trigger_check(false);
    }
  }

  void open_last_notified_release_page() {
    try {
      const std::string &url = state.last_notified_url;
      if (!url.empty()) {
        platf::open_url(url);
      }
    } catch (...) {
      // swallow errors; opening the URL is best-effort
    }
  }
}  // namespace update