mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-11-04 01:00:05 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			260 lines
		
	
	
	
		
			6.6 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			260 lines
		
	
	
	
		
			6.6 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
#include <scwx/qt/manager/update_manager.hpp>
 | 
						|
#include <scwx/util/logger.hpp>
 | 
						|
 | 
						|
#include <mutex>
 | 
						|
#include <regex>
 | 
						|
 | 
						|
#include <boost/json.hpp>
 | 
						|
#include <cpr/cpr.h>
 | 
						|
 | 
						|
namespace scwx
 | 
						|
{
 | 
						|
namespace qt
 | 
						|
{
 | 
						|
namespace manager
 | 
						|
{
 | 
						|
 | 
						|
static const std::string logPrefix_ = "scwx::qt::manager::update_manager";
 | 
						|
static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
 | 
						|
 | 
						|
static const std::string kGithubApiBase {"https://api.github.com"};
 | 
						|
static const std::string kScwxReleaseEndpoint {
 | 
						|
   kGithubApiBase + "/repos/dpaulat/supercell-wx/releases"};
 | 
						|
 | 
						|
class UpdateManager::Impl
 | 
						|
{
 | 
						|
public:
 | 
						|
   explicit Impl(UpdateManager* self) : self_ {self} {}
 | 
						|
 | 
						|
   ~Impl() {}
 | 
						|
 | 
						|
   static std::string        GetVersionString(const std::string& releaseName);
 | 
						|
   static boost::json::value ParseResponseText(const std::string& s);
 | 
						|
 | 
						|
   size_t PopulateReleases();
 | 
						|
   size_t AddReleases(const boost::json::value& json);
 | 
						|
   std::pair<std::vector<types::gh::Release>::iterator, std::string>
 | 
						|
   FindLatestRelease();
 | 
						|
 | 
						|
   UpdateManager* self_;
 | 
						|
 | 
						|
   std::mutex updateMutex_ {};
 | 
						|
 | 
						|
   std::vector<types::gh::Release> releases_ {};
 | 
						|
   types::gh::Release              latestRelease_ {};
 | 
						|
   std::string                     latestVersion_ {};
 | 
						|
};
 | 
						|
 | 
						|
UpdateManager::UpdateManager() : p(std::make_unique<Impl>(this)) {}
 | 
						|
UpdateManager::~UpdateManager() = default;
 | 
						|
 | 
						|
types::gh::Release UpdateManager::latest_release() const
 | 
						|
{
 | 
						|
   return p->latestRelease_;
 | 
						|
}
 | 
						|
 | 
						|
std::string UpdateManager::latest_version() const
 | 
						|
{
 | 
						|
   return p->latestVersion_;
 | 
						|
}
 | 
						|
 | 
						|
std::string
 | 
						|
UpdateManager::Impl::GetVersionString(const std::string& releaseName)
 | 
						|
{
 | 
						|
   static const std::regex re {"\\d+\\.\\d+\\.\\d+"};
 | 
						|
   std::string             versionString {};
 | 
						|
   std::smatch             m;
 | 
						|
 | 
						|
   std::regex_search(releaseName, m, re);
 | 
						|
 | 
						|
   if (!m.empty())
 | 
						|
   {
 | 
						|
      versionString = m[0].str();
 | 
						|
   }
 | 
						|
 | 
						|
   return versionString;
 | 
						|
}
 | 
						|
 | 
						|
boost::json::value UpdateManager::Impl::ParseResponseText(const std::string& s)
 | 
						|
{
 | 
						|
   boost::json::stream_parser p;
 | 
						|
   boost::json::error_code    ec;
 | 
						|
 | 
						|
   p.write(s, ec);
 | 
						|
   if (ec)
 | 
						|
   {
 | 
						|
      logger_->warn("{}", ec.message());
 | 
						|
      return nullptr;
 | 
						|
   }
 | 
						|
 | 
						|
   p.finish(ec);
 | 
						|
   if (ec)
 | 
						|
   {
 | 
						|
      logger_->warn("{}", ec.message());
 | 
						|
      return nullptr;
 | 
						|
   }
 | 
						|
 | 
						|
   return p.release();
 | 
						|
}
 | 
						|
 | 
						|
bool UpdateManager::CheckForUpdates(const std::string& currentVersion)
 | 
						|
{
 | 
						|
   std::unique_lock lock(p->updateMutex_);
 | 
						|
 | 
						|
   logger_->info("Checking for updates");
 | 
						|
 | 
						|
   // Query GitHub for releases
 | 
						|
   size_t numReleases = p->PopulateReleases();
 | 
						|
   bool   newRelease  = false;
 | 
						|
 | 
						|
   // If GitHub returned valid releases
 | 
						|
   if (numReleases > 0)
 | 
						|
   {
 | 
						|
      // Get the latest release
 | 
						|
      auto [latestRelease, latestVersion] = p->FindLatestRelease();
 | 
						|
 | 
						|
      // Validate the latest release, and compare to the current version
 | 
						|
      if (latestRelease != p->releases_.end() && latestVersion > currentVersion)
 | 
						|
      {
 | 
						|
         logger_->info("An update is available: {}", latestVersion);
 | 
						|
 | 
						|
         p->latestRelease_ = *latestRelease;
 | 
						|
         p->latestVersion_ = latestVersion;
 | 
						|
         newRelease        = true;
 | 
						|
         emit UpdateAvailable(latestVersion, *latestRelease);
 | 
						|
      }
 | 
						|
   }
 | 
						|
 | 
						|
   return newRelease;
 | 
						|
}
 | 
						|
 | 
						|
size_t UpdateManager::Impl::PopulateReleases()
 | 
						|
{
 | 
						|
   static constexpr size_t perPage =
 | 
						|
      100u;                // The number of results per page (max 100)
 | 
						|
   size_t page       = 1u; // Page number of the results to fetch
 | 
						|
   size_t numResults = 0u;
 | 
						|
 | 
						|
   static const std::string perPageString {fmt::format("{}", perPage)};
 | 
						|
 | 
						|
   // Clear any existing releases
 | 
						|
   releases_.clear();
 | 
						|
 | 
						|
   do
 | 
						|
   {
 | 
						|
      const std::string pageString {fmt::format("{}", page)};
 | 
						|
 | 
						|
      cpr::Response r = cpr::Get(
 | 
						|
         cpr::Url {kScwxReleaseEndpoint},
 | 
						|
         cpr::Parameters {{"per_page", perPageString}, {"page", pageString}},
 | 
						|
         cpr::Header {{"accept", "application/vnd.github+json"},
 | 
						|
                      {"X-GitHub-Api-Version", "2022-11-28"}});
 | 
						|
 | 
						|
      // Successful REST API query
 | 
						|
      if (r.status_code == 200)
 | 
						|
      {
 | 
						|
         boost::json::value json = Impl::ParseResponseText(r.text);
 | 
						|
         if (json == nullptr)
 | 
						|
         {
 | 
						|
            logger_->warn("Response not JSON: {}", r.header["content-type"]);
 | 
						|
            break;
 | 
						|
         }
 | 
						|
 | 
						|
         // Add results from response
 | 
						|
         size_t newResults = AddReleases(json);
 | 
						|
         numResults += newResults;
 | 
						|
 | 
						|
         if (newResults < perPage)
 | 
						|
         {
 | 
						|
            // We have reached the last page of results
 | 
						|
            break;
 | 
						|
         }
 | 
						|
      }
 | 
						|
      else
 | 
						|
      {
 | 
						|
         logger_->warn(
 | 
						|
            "Invalid API response: [{}] {}", r.status_code, r.error.message);
 | 
						|
         break;
 | 
						|
      }
 | 
						|
 | 
						|
      // Check page is less than 100, this is to prevent an infinite loop
 | 
						|
   } while (++page < 100);
 | 
						|
 | 
						|
   return numResults;
 | 
						|
}
 | 
						|
 | 
						|
size_t UpdateManager::Impl::AddReleases(const boost::json::value& json)
 | 
						|
{
 | 
						|
   // Parse releases
 | 
						|
   std::vector<types::gh::Release> newReleases {};
 | 
						|
   try
 | 
						|
   {
 | 
						|
      newReleases =
 | 
						|
         boost::json::value_to<std::vector<types::gh::Release>>(json);
 | 
						|
   }
 | 
						|
   catch (const std::exception& ex)
 | 
						|
   {
 | 
						|
      logger_->warn("Error parsing JSON: {}", ex.what());
 | 
						|
   }
 | 
						|
 | 
						|
   size_t newReleaseCount = newReleases.size();
 | 
						|
 | 
						|
   // Add releases to the current list
 | 
						|
   releases_.insert(releases_.end(), newReleases.begin(), newReleases.end());
 | 
						|
 | 
						|
   return newReleaseCount;
 | 
						|
}
 | 
						|
 | 
						|
std::pair<std::vector<types::gh::Release>::iterator, std::string>
 | 
						|
UpdateManager::Impl::FindLatestRelease()
 | 
						|
{
 | 
						|
   // Initialize the latest release to the end iterator
 | 
						|
   std::vector<types::gh::Release>::iterator latestRelease = releases_.end();
 | 
						|
   std::string                               latestReleaseVersion {};
 | 
						|
 | 
						|
   for (auto it = releases_.begin(); it != releases_.end(); ++it)
 | 
						|
   {
 | 
						|
      if (it->draft_ || it->prerelease_)
 | 
						|
      {
 | 
						|
         // Skip drafts and prereleases
 | 
						|
         continue;
 | 
						|
      }
 | 
						|
 | 
						|
      // Get the version string of the current release
 | 
						|
      std::string currentVersion {GetVersionString(it->name_)};
 | 
						|
 | 
						|
      // If not set, or current version is lexographically newer
 | 
						|
      if (latestRelease == releases_.end() ||
 | 
						|
          currentVersion > latestReleaseVersion)
 | 
						|
      {
 | 
						|
         // Update the latest release
 | 
						|
         latestRelease        = it;
 | 
						|
         latestReleaseVersion = currentVersion;
 | 
						|
      }
 | 
						|
   }
 | 
						|
 | 
						|
   return {latestRelease, latestReleaseVersion};
 | 
						|
}
 | 
						|
 | 
						|
std::shared_ptr<UpdateManager> UpdateManager::Instance()
 | 
						|
{
 | 
						|
   static std::weak_ptr<UpdateManager> updateManagerReference_ {};
 | 
						|
   static std::mutex                   instanceMutex_ {};
 | 
						|
 | 
						|
   std::unique_lock lock(instanceMutex_);
 | 
						|
 | 
						|
   std::shared_ptr<UpdateManager> updateManager =
 | 
						|
      updateManagerReference_.lock();
 | 
						|
 | 
						|
   if (updateManager == nullptr)
 | 
						|
   {
 | 
						|
      updateManager           = std::make_shared<UpdateManager>();
 | 
						|
      updateManagerReference_ = updateManager;
 | 
						|
   }
 | 
						|
 | 
						|
   return updateManager;
 | 
						|
}
 | 
						|
 | 
						|
} // namespace manager
 | 
						|
} // namespace qt
 | 
						|
} // namespace scwx
 |