diff --git a/PiPedalCommon/src/include/json.hpp b/PiPedalCommon/src/include/json.hpp index e477411..23ef873 100644 --- a/PiPedalCommon/src/include/json.hpp +++ b/PiPedalCommon/src/include/json.hpp @@ -33,6 +33,7 @@ #include #include #include +#include #define DECLARE_JSON_MAP(CLASSNAME) \ static pipedal::json_map::storage_type jmap @@ -367,6 +368,7 @@ namespace pipedal { os << value; } + void write (const std::chrono::system_clock::time_point &time); private: static void throw_encoding_error(); @@ -995,6 +997,8 @@ namespace pipedal if (is_.fail()) throw JsonException("Invalid format."); } + void read(std::chrono::system_clock::time_point *value); + template void read(std::vector *value) { diff --git a/PiPedalCommon/src/json.cpp b/PiPedalCommon/src/json.cpp index ee6b163..5b55fc6 100644 --- a/PiPedalCommon/src/json.cpp +++ b/PiPedalCommon/src/json.cpp @@ -632,6 +632,60 @@ void json_reader::throw_format_error(const char*error) } +static std::string timePointToISO8601(const std::chrono::system_clock::time_point &tp) +{ + auto tt = std::chrono::system_clock::to_time_t(tp); + std::tm tm = *std::gmtime(&tt); + std::stringstream ss; + ss << std::put_time(&tm,"%Y-%m-%dT%H:%M:%S") << "Z"; + return ss.str(); + +} +static std::chrono::system_clock::time_point ISO8601ToTimePoint(const std::string&timeString) +{ + if (timeString.empty()) + { + return std::chrono::system_clock::time_point(); + } + if (timeString.back() != 'Z') + { + throw std::runtime_error("Time string is not in UTC Z timezone. Time-zones are not supported."); + } + + std::tm tm = {}; + std::istringstream ss(timeString); + ss >> std::get_time(&tm,"%Y-%m-%dT%H:%M:%S"); + if (ss.fail()) + { + throw std::runtime_error("Failed to parse ISO 8601 time string."); + } + time_t tt; + #ifdef _WIN32 + tt = _mkgmtime(&tm); + #else + tt = timegm(&tm); + #endif + if (tt == -1) + { + throw std::runtime_error("Failed to conver ISO 8601 time string."); + } + return std::chrono::system_clock::from_time_t(tt); +} + +void json_writer::write (const std::chrono::system_clock::time_point &time) +{ + std::string timeString = timePointToISO8601(time); + write(timeString); +} + +void json_reader::read(std::chrono::system_clock::time_point *value) +{ + std::string timeString; + read(&timeString); + *value = ISO8601ToTimePoint(timeString); +} + + // void json_writer::write(const json_variant &value) // { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ba8aede..d8abe0f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -151,7 +151,7 @@ set (PIPEDAL_SOURCES HotspotManager.cpp HotspotManager.hpp UpdateResults.cpp UpdateResults.hpp UpdaterSecurity.hpp - Updater.cpp Updater.hpp + Updater.cpp Updater.hpp UpdaterStatus.hpp Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp WebServerConfig.cpp WebServerConfig.hpp Locale.hpp Locale.cpp diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 90005b8..22f8ed7 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022-2023 Robin Davies +// Copyright (c) 2022-2024 Robin Davies // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -38,6 +38,7 @@ #include "HotspotManager.hpp" #include "DBusToLv2Log.hpp" #include "SysExec.hpp" +#include "Updater.hpp" #ifndef NO_MLOCK #include @@ -69,7 +70,8 @@ PiPedalModel::PiPedalModel() : pluginHost(), atomConverter(pluginHost.GetMapFeature()) { - this->currentUpdateStatus = updater.GetCurrentStatus(); + this->updater = Updater::Create(); + this->currentUpdateStatus = updater->GetCurrentStatus(); this->pedalboard = Pedalboard::MakeDefault(); #if JACK_HOST this->jackServerSettings = this->storage.GetJackServerSettings(); // to get onboarding flag. @@ -77,7 +79,7 @@ PiPedalModel::PiPedalModel() #else this->jackServerSettings = this->storage.GetJackServerSettings(); #endif - updater.SetUpdateListener( + updater->SetUpdateListener( [this](const UpdateStatus &updateStatus) { this->OnUpdateStatusChanged(updateStatus); @@ -2212,24 +2214,24 @@ void PiPedalModel::FireUpdateStatusChanged(const UpdateStatus &updateStatus) UpdateStatus PiPedalModel::GetUpdateStatus() { std::lock_guard lock(mutex); - return updater.GetCurrentStatus(); + return updater->GetCurrentStatus(); } void PiPedalModel::UpdateNow(const std::string &updateUrl) { std::lock_guard lock(mutex); std::filesystem::path fileName, signatureName; - updater.DownloadUpdate(updateUrl, &fileName, &signatureName); + updater->DownloadUpdate(updateUrl, &fileName, &signatureName); adminClient.InstallUpdate(fileName); } void PiPedalModel::ForceUpdateCheck() { - updater.ForceUpdateCheck(); + updater->ForceUpdateCheck(); } void PiPedalModel::SetUpdatePolicy(UpdatePolicyT updatePolicy) { - updater.SetUpdatePolicy(updatePolicy); + updater->SetUpdatePolicy(updatePolicy); } static bool HasAlsaDevice(const std::vector devices, const std::string &deviceId) diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 0eb3575..1487fd7 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -19,6 +19,7 @@ #pragma once #include +#include "UpdaterStatus.hpp" #include "PluginHost.hpp" #include "GovernorSettings.hpp" #include "Pedalboard.hpp" @@ -30,7 +31,6 @@ #include #include #include "Banks.hpp" -#include "Updater.hpp" #include "PiPedalConfiguration.hpp" #include "JackServerSettings.hpp" #include "WifiConfigSettings.hpp" @@ -48,6 +48,7 @@ namespace pipedal struct RealtimeMidiProgramRequest; struct RealtimeNextMidiProgramRequest; class Lv2PluginChangeMonitor; + class Updater; class IPiPedalModelSubscriber { @@ -101,8 +102,8 @@ namespace pipedal private: std::unique_ptr hotspotManager; + std::unique_ptr updater; UpdateStatus currentUpdateStatus; - Updater updater; void OnUpdateStatusChanged(const UpdateStatus&updateStatus); std::function restartListener; diff --git a/src/Updater.cpp b/src/Updater.cpp index 3733345..3511436 100644 --- a/src/Updater.cpp +++ b/src/Updater.cpp @@ -35,9 +35,9 @@ #include "UpdaterSecurity.hpp" #include "SysExec.hpp" #include "ofstream_synced.hpp" +#include #include "TemporaryFile.hpp" -#include "ofstream_synced.hpp" -#include +#include using namespace pipedal; namespace fs = std::filesystem; @@ -48,6 +48,94 @@ namespace fs = std::filesystem; #undef TEST_UPDATE // do NOT leat this leak into a production build! #endif +namespace pipedal { + class GithubResponseHeaders + { + public: + GithubResponseHeaders() {} + GithubResponseHeaders(const std::filesystem::path path); + int code_ = -1; + std::chrono::system_clock::time_point date_; + std::chrono::system_clock::time_point ratelimit_reset_; + uint64_t ratelimit_limit_ = 60; + uint64_t ratelimit_remaining_ = 60; + uint64_t ratelimit_used_ = 0; + std::string ratelimit_resource_; + bool limit_exceeded() const { return code_ != 200 && ratelimit_limit_ != 0 && ratelimit_limit_ == ratelimit_used_; } + void Load(); + void Save(); + DECLARE_JSON_MAP(GithubResponseHeaders); + + private: + static std::filesystem::path FILENAME; + }; +} + +class pipedal::UpdaterImpl : public Updater +{ +public: + UpdaterImpl(); + virtual ~UpdaterImpl() noexcept; + + virtual void SetUpdateListener(UpdateListener &&listener) override; + virtual void CheckNow() override; + virtual void Stop(); + + virtual UpdatePolicyT GetUpdatePolicy() override; + virtual void SetUpdatePolicy(UpdatePolicyT updatePolicy) override; + virtual void ForceUpdateCheck() override; + virtual void DownloadUpdate(const std::string &url, std::filesystem::path *file, std::filesystem::path *signatureFile) override; + virtual UpdateStatus GetCurrentStatus() const override { return this->currentResult; } + +private: + GithubResponseHeaders githubResponseHeaders; + + using clock = std::chrono::steady_clock; + + void RetryAfter(clock::duration delay); + + template + void RetryAfter(std::chrono::duration duration) + { + RetryAfter(std::chrono::duration_cast(duration)); + } + + void SaveRetryTime(const std::chrono::system_clock::time_point &time); + clock::time_point LoadRetryTime(); + + std::string GetUpdateFilename(const std::string &url); + std::string GetSignatureUrl(const std::string &url); + + UpdatePolicyT updatePolicy = UpdatePolicyT::ReleaseOrBeta; + using UpdateReleasePredicate = std::function; + + UpdateRelease getUpdateRelease( + const std::vector &githubReleases, + const std::string ¤tVersion, + const UpdateReleasePredicate &predicate); + + UpdateStatus cachedUpdateStatus; + bool stopped = false; + + int event_reader = -1; + int event_writer = -1; + void ThreadProc(); + void CheckForUpdate(bool useCache); + UpdateListener listener; + + std::unique_ptr thread; + std::mutex mutex; + + bool hasInfo = false; + UpdateStatus currentResult; + static clock::duration updateRate; + clock::time_point updateRetryTime; +}; +Updater::ptr Updater::Create() +{ + return std::make_unique(); +} + static constexpr uint64_t CLOSE_EVENT = 0; static constexpr uint64_t CHECK_NOW_EVENT = 1; static constexpr uint64_t UNCACHED_CHECK_NOW_EVENT = 2; @@ -55,7 +143,7 @@ static constexpr uint64_t UNCACHED_CHECK_NOW_EVENT = 2; static std::filesystem::path WORKING_DIRECTORY = "/var/pipedal/updates"; static std::filesystem::path UPDATE_STATUS_CACHE_FILE = WORKING_DIRECTORY / "updateStatus.json"; -Updater::clock::duration Updater::updateRate = std::chrono::duration_cast(std::chrono::days(1)); +UpdaterImpl::clock::duration UpdaterImpl::updateRate = std::chrono::duration_cast(std::chrono::days(1)); static std::chrono::system_clock::duration CACHE_DURATION = std::chrono::duration_cast(std::chrono::minutes(30)); std::mutex cacheMutex; @@ -101,8 +189,9 @@ static void SetCachedUpdateStatus(UpdateStatus &updateStatus) Lv2Log::error(SS("Unable to write cached UpdateStatus. " << e.what())); } } -Updater::Updater() +UpdaterImpl::UpdaterImpl() { + this->githubResponseHeaders.Load(); cachedUpdateStatus = GetCachedUpdateStatus(); this->updatePolicy = cachedUpdateStatus.UpdatePolicy(); currentResult = cachedUpdateStatus; @@ -120,11 +209,17 @@ Updater::Updater() { ThreadProc(); }); CheckNow(); } -Updater::~Updater() +UpdaterImpl::~UpdaterImpl() { - Stop(); + try + { + Stop(); + } + catch (const std::exception & /*ignored*/) + { + } } -void Updater::Stop() +void UpdaterImpl::Stop() { if (stopped) { @@ -152,13 +247,13 @@ void Updater::Stop() } } -void Updater::CheckNow() +void UpdaterImpl::CheckNow() { uint64_t value = CHECK_NOW_EVENT; write(this->event_writer, &value, sizeof(uint64_t)); } -void Updater::SetUpdateListener(UpdateListener &&listener) +void UpdaterImpl::SetUpdateListener(UpdateListener &&listener) { std::lock_guard lock{mutex}; this->listener = listener; @@ -168,16 +263,16 @@ void Updater::SetUpdateListener(UpdateListener &&listener) } } -static void LogNextStartTime(const std::chrono::steady_clock::time_point &clockTime) { +static void LogNextStartTime(const std::chrono::steady_clock::time_point &clockTime) +{ using namespace std::chrono; // convert to system_clock; - auto delay = clockTime-steady_clock::now(); + auto delay = clockTime - steady_clock::now(); auto systemTime = system_clock::now() + duration_cast(delay); auto t = system_clock::to_time_t(systemTime); - Lv2Log::info(SS("Updater: Next update check at " << std::put_time(std::localtime(&t),"%Y-%m-%d %H:%M:%S"))); - + Lv2Log::info(SS("Updater: Next update check at " << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S"))); } -void Updater::ThreadProc() +void UpdaterImpl::ThreadProc() { SetThreadName("UpdateMonitor"); struct pollfd pfd; @@ -186,7 +281,8 @@ void Updater::ThreadProc() auto lastRetryTime = clock::time_point(); this->updateRetryTime = LoadRetryTime(); - try { + try + { while (true) { if (updateRetryTime != lastRetryTime) // has retry time changed? @@ -197,14 +293,13 @@ void Updater::ThreadProc() LogNextStartTime(updateRetryTime); } } - auto clockDelay = this->updateRetryTime-clock::now(); + auto clockDelay = this->updateRetryTime - clock::now(); auto delayMs = std::chrono::duration_cast(clockDelay).count(); if (delayMs > std::numeric_limits::max()) // could be as little as 32 seconds on a 16-bit int system. { delayMs = std::numeric_limits::max(); } - int ret = poll(&pfd, 1, (int)delayMs); // 1000 ms timeout if (ret == -1) @@ -228,7 +323,7 @@ void Updater::ThreadProc() { if (value == CHECK_NOW_EVENT) { - + CheckForUpdate(true); } else if (value == UNCACHED_CHECK_NOW_EVENT) @@ -242,37 +337,41 @@ void Updater::ThreadProc() } } } - } catch (const std::exception&e) + } + catch (const std::exception &e) { Lv2Log::error(SS("Updater: Service thread terminated abnormally: " << e.what())); } } -class GithubAsset +namespace pipedal { -public: - GithubAsset(json_variant &v); - std::string name; - std::string browser_download_url; - std::string updated_at; -}; -class GithubRelease -{ -public: - GithubRelease(json_variant &v); + class GithubAsset + { + public: + GithubAsset(json_variant &v); + std::string name; + std::string browser_download_url; + std::string updated_at; + }; + class GithubRelease + { + public: + GithubRelease(json_variant &v); - const GithubAsset *GetDownloadForCurrentArchitecture() const; - const GithubAsset *GetGpgKeyForAsset(const std::string &name) const; + const GithubAsset *GetDownloadForCurrentArchitecture() const; + const GithubAsset *GetGpgKeyForAsset(const std::string &name) const; - bool draft = true; - bool prerelease = true; - std::string name; - std::string url; - std::string version; - std::string body; - std::vector assets; - std::string published_at; -}; + bool draft = true; + bool prerelease = true; + std::string name; + std::string url; + std::string version; + std::string body; + std::vector assets; + std::string published_at; + }; +} GithubAsset::GithubAsset(json_variant &v) { @@ -320,7 +419,7 @@ static std::string justTheVersion(const std::string &assetName) return t[1]; } -int compareVersions(const std::string &l, const std::string &r) +static int compareVersions(const std::string &l, const std::string &r) { std::stringstream sl(l); std::stringstream sr(r); @@ -380,19 +479,7 @@ static std::string normalizeReleaseName(const std::string &releaseName) return result; } -static bool IsCacheValid(const UpdateStatus &updateStatus) -{ - if (!updateStatus.IsValid() || !updateStatus.IsOnline()) - { - return false; - } - auto now = std::chrono::system_clock::now(); - auto validStart = updateStatus.LastUpdateTime(); - auto validEnd = validStart + CACHE_DURATION; - return now >= validStart && now < validEnd; -} - -UpdateRelease Updater::getUpdateRelease( +UpdateRelease UpdaterImpl::getUpdateRelease( const std::vector &githubReleases, const std::string ¤tVersion, const UpdateReleasePredicate &predicate) @@ -452,111 +539,157 @@ static void CheckUpdateHttpResponse(std::string errorCode) } } - -static std::chrono::system_clock::time_point http_date_to_time_point(const std::string& http_date) { +static std::chrono::system_clock::time_point http_date_to_time_point(const std::string &http_date) +{ std::tm tm = {}; std::istringstream ss(http_date); - + ss >> std::get_time(&tm, "%a, %d %b %Y %H:%M:%S GMT"); - - if (ss.fail()) { + + if (ss.fail()) + { throw std::runtime_error("Failed to parse HTTP date: " + http_date); } - - return std::chrono::system_clock::from_time_t(std::mktime(&tm)); + auto tt = timegm(&tm); + return std::chrono::system_clock::from_time_t(tt); } -class GitResponseHeaders { -public: - GitResponseHeaders(const std::filesystem::path path) + +JSON_MAP_BEGIN(GithubResponseHeaders) +JSON_MAP_REFERENCE(GithubResponseHeaders, date) +JSON_MAP_REFERENCE(GithubResponseHeaders, ratelimit_reset) +JSON_MAP_REFERENCE(GithubResponseHeaders, ratelimit_limit) +JSON_MAP_REFERENCE(GithubResponseHeaders, ratelimit_remaining) +JSON_MAP_REFERENCE(GithubResponseHeaders, ratelimit_used) +JSON_MAP_REFERENCE(GithubResponseHeaders, ratelimit_resource) +JSON_MAP_END(); + +std::filesystem::path GithubResponseHeaders::FILENAME = WORKING_DIRECTORY / "githubHeaders.json"; +void GithubResponseHeaders::Load() +{ + std::ifstream f(FILENAME); + if (f.is_open()) { - // HTTP/2 403 - // date: Fri, 13 Sep 2024 17:31:22 GMT - //... - // x-ratelimit-limit: 60 - // x-ratelimit-remaining: 0 - // x-ratelimit-reset: 1726250807 - // x-ratelimit-resource: core - // x-ratelimit-used: 60 - std::ifstream f {path}; - if (!f.is_open()) + try { - throw std::runtime_error(SS("Can't open file " << path << ".")); + json_reader reader(f); + reader.read(this); } - std::string http; - f >> http >> code; - std::string line; - std::getline(f,line); - - while (true) + catch (const std::exception &e) { - std::getline(f,line); - auto trimPos = line.find_last_not_of("\r\n"); - if (trimPos != std::string::npos) { - line = line.substr(trimPos+1); - } + Lv2Log::error(SS("Invalid file format: " << FILENAME << ".")); + *this = GithubResponseHeaders(); + } + } +} +void GithubResponseHeaders::Save() +{ + std::filesystem::create_directories(WORKING_DIRECTORY); + ofstream_synced f(FILENAME); + if (!f.is_open()) + { + Lv2Log::error(SS("Can't write to " << FILENAME << ".")); + } + else + { + json_writer writer(f); + writer.write(this); + } +} - if (!f) break; - auto pos = line.find(':'); - if (pos != std::string::npos) +GithubResponseHeaders::GithubResponseHeaders(const std::filesystem::path path) +{ + // HTTP/2 403 + // date: Fri, 13 Sep 2024 17:31:22 GMT + //... + // x-ratelimit-limit: 60 + // x-ratelimit-remaining: 0 + // x-ratelimit-reset: 1726250807 + // x-ratelimit-resource: core + // x-ratelimit-used: 60 + std::ifstream f{path}; + if (!f.is_open()) + { + throw std::runtime_error(SS("Can't open file " << path << ".")); + } + std::string http; + f >> http >> code_; + std::string line; + std::getline(f, line); + + while (true) + { + std::getline(f, line); + auto trimPos = line.find_last_not_of("\r\n"); + if (trimPos != std::string::npos) + { + line = line.substr(0,trimPos + 1); + } + + if (!f) + break; + auto pos = line.find(':'); + if (pos != std::string::npos) + { + std::string tag = line.substr(0, pos); + ++pos; + while (pos < line.length() && line[pos] == ' ') { - std::string tag = line.substr(0,pos); ++pos; - while (pos < line.length() && line[pos] == ' ') - { - ++pos; - } - std::string value = line.substr(pos); - uint64_t *pResult = nullptr; - if (tag =="date") - { - this->date = http_date_to_time_point(value); - } else if (tag == "x-ratelimit-limit") - { - pResult = &ratelimit_limit; - } else if (tag == "x-ratelimit-remaining") - { - pResult = &ratelimit_remaining; - } else if (tag == "x-ratelimit-reset") - { - std::istringstream ss {value}; - uint64_t reset = 0; - ss >> reset; - auto secs = std::chrono::seconds(reset); // value is seconds in UTC Unix epoch. - this->ratelimit_reset = std::chrono::system_clock::time_point(secs); - } else if (tag == "x-ratelimit-resource") - { - ratelimit_resource = value; - } else if (tag == "x-ratelimit-used") - { - pResult = &ratelimit_used; - } - if (pResult) - { - std::istringstream ss { value}; - ss >> *pResult; - } + } + std::string value = line.substr(pos); + uint64_t *pResult = nullptr; + if (tag == "date") + { + this->date_ = http_date_to_time_point(value); + } + else if (tag == "x-ratelimit-limit") + { + pResult = &ratelimit_limit_; + } + else if (tag == "x-ratelimit-remaining") + { + pResult = &ratelimit_remaining_; + } + else if (tag == "x-ratelimit-reset") + { + std::istringstream ss{value}; + uint64_t reset = 0; + ss >> reset; + auto secs = std::chrono::seconds(reset); // value is seconds in UTC Unix epoch. + this->ratelimit_reset_ = std::chrono::system_clock::time_point(secs); + } + else if (tag == "x-ratelimit-resource") + { + ratelimit_resource_ = value; + } + else if (tag == "x-ratelimit-used") + { + pResult = &ratelimit_used_; + } + if (pResult) + { + std::istringstream ss{value}; + ss >> *pResult; } } } - int code = -1; - std::chrono::system_clock::time_point date; - std::chrono::system_clock::time_point ratelimit_reset; - uint64_t ratelimit_limit = 0; - uint64_t ratelimit_remaining = 0; - uint64_t ratelimit_used = 0; - std::string ratelimit_resource; - bool limit_exceeded() const { return code != 200 && ratelimit_limit != 0 && ratelimit_limit == ratelimit_used; } +} -}; -void Updater::CheckForUpdate(bool useCache) +void UpdaterImpl::CheckForUpdate(bool useCache) { UpdateStatus updateResult; + // if we've used up too many actual github requests, used the cached version even if the request is non-cached. + if (githubResponseHeaders.ratelimit_remaining_ < 3 * githubResponseHeaders.ratelimit_limit_ / 4) + { + useCache = true; + } + if (useCache) { - std::lock_guard lock{mutex}; + if (clock::now() < this->updateRetryTime) { - if (IsCacheValid(cachedUpdateStatus)) + std::lock_guard lock{mutex}; { this->currentResult = cachedUpdateStatus; this->currentResult.UpdatePolicy(this->updatePolicy); @@ -568,18 +701,6 @@ void Updater::CheckForUpdate(bool useCache) } } } - if (clock::now() < this->updateRetryTime) - { - // forced or not, do NOT hit the githup api before we're supposed to. - std::lock_guard lock(mutex); - this->currentResult = cachedUpdateStatus; - this->currentResult.UpdatePolicy(this->updatePolicy); - if (listener) - { - listener(this->currentResult); - } - return; - } // set default next retry time. github failures may postpone even further. @@ -589,7 +710,7 @@ void Updater::CheckForUpdate(bool useCache) responseOption = "-w \"%%{response_code}\""; // windows shell requires doubling of the %%. #endif - TemporaryFile headerFile { WORKING_DIRECTORY }; + TemporaryFile headerFile{WORKING_DIRECTORY}; std::string args = SS("-s -L " << GITHUB_RELEASES_URL << " -D " << headerFile.str()); updateResult.errorMessage_ = ""; @@ -607,22 +728,21 @@ void Updater::CheckForUpdate(bool useCache) // hard throttling for github. RetryAfter(std::chrono::duration_cast(std::chrono::hours(8))); - GitResponseHeaders githubHeaders { headerFile.Path()}; + GithubResponseHeaders githubHeaders{headerFile.Path()}; + this->githubResponseHeaders = githubHeaders; + this->githubResponseHeaders.Save(); - if (githubHeaders.code != 200) + if (githubHeaders.code_ != 200) { if ( - githubHeaders.ratelimit_limit != 0 && - githubHeaders.ratelimit_limit == githubHeaders.ratelimit_used) + githubHeaders.ratelimit_limit_ != 0 && + githubHeaders.ratelimit_limit_ == githubHeaders.ratelimit_used_) { - std::time_t time = std::chrono::system_clock::to_time_t(githubHeaders.ratelimit_reset); + std::time_t time = std::chrono::system_clock::to_time_t(githubHeaders.ratelimit_reset_); std::string strTime = std::ctime(&time); - - Lv2Log::warning(SS("Updater: github rate limit exceeded. Retry at " << strTime)); - - this->RetryAfter(githubHeaders.ratelimit_reset-githubHeaders.date); - return; + this->RetryAfter(githubHeaders.ratelimit_reset_ - githubHeaders.date_); + throw std::runtime_error(SS("Github API rate limit exceeded. Retrying at " << strTime)); } } @@ -639,16 +759,18 @@ void Updater::CheckForUpdate(bool useCache) // an HTML error. updateResult.isOnline_ = false; auto o = vResult.as_object(); - std::string message = "Unknown error."; + std::string message = "Unknown error."; if (o->at("message").is_string()) { - message = o->at("message").as_string(); + message = o->at("message").as_string(); } - throw std::runtime_error(SS("Github Service error: " << message )); - } else if (!vResult.is_array()) + throw std::runtime_error(SS("Github Service error: " << message)); + } + else if (!vResult.is_array()) { throw std::runtime_error("Invalid file format error."); - } else + } + else { json_variant::array_ptr vArray = vResult.as_array(); @@ -718,7 +840,10 @@ void Updater::CheckForUpdate(bool useCache) { std::lock_guard lock{mutex}; updateResult.UpdatePolicy(this->updatePolicy); - this->currentResult = updateResult; + if (updateResult.isOnline_ && updateResult.isValid_) + { + this->currentResult = updateResult; + } SetCachedUpdateStatus(this->currentResult); if (listener) { @@ -749,12 +874,12 @@ bool UpdateStatus::operator==(const UpdateStatus &other) const (devRelease_ == other.devRelease_); } -UpdatePolicyT Updater::GetUpdatePolicy() +UpdatePolicyT UpdaterImpl::GetUpdatePolicy() { std::lock_guard lock{mutex}; return updatePolicy; } -void Updater::SetUpdatePolicy(UpdatePolicyT updatePolicy) +void UpdaterImpl::SetUpdatePolicy(UpdatePolicyT updatePolicy) { std::lock_guard lock{mutex}; if (updatePolicy == this->updatePolicy) @@ -767,7 +892,7 @@ void Updater::SetUpdatePolicy(UpdatePolicyT updatePolicy) listener(currentResult); } } -void Updater::ForceUpdateCheck() +void UpdaterImpl::ForceUpdateCheck() { uint64_t value = UNCACHED_CHECK_NOW_EVENT; write(this->event_writer, &value, sizeof(uint64_t)); @@ -845,7 +970,7 @@ UpdateRelease::UpdateRelease() { } -std::string Updater::GetSignatureUrl(const std::string &url) +std::string UpdaterImpl::GetSignatureUrl(const std::string &url) { // partialy whitelisting, partly avoiding having to parse a URL. @@ -864,7 +989,7 @@ std::string Updater::GetSignatureUrl(const std::string &url) throw std::runtime_error("Permission denied. No signature URL."); } -std::string Updater::GetUpdateFilename(const std::string &url) +std::string UpdaterImpl::GetUpdateFilename(const std::string &url) { // partialy whitelisting, partly avoiding having to parse a URL. @@ -1023,7 +1148,6 @@ void Updater::ValidateSignature(const std::filesystem::path &file, const std::fi } std::string keyId = getFingerprint(gpgText); - if (keyId != UPDATE_GPG_FINGERPRINT && keyId != UPDATE_GPG_FINGERPRINT2) { throw std::runtime_error(SS("Update signature has the wrong id: " << keyId)); @@ -1073,7 +1197,7 @@ static void checkCurlHttpResponse(std::string errorCode) throw std::runtime_error(message); } } -void Updater::RetryAfter(clock::duration delay) +void UpdaterImpl::RetryAfter(clock::duration delay) { namespace chron = std::chrono; clock::duration hours24 = chron::duration_cast(chron::hours(24)); @@ -1081,33 +1205,32 @@ void Updater::RetryAfter(clock::duration delay) { delay = chron::duration_cast(chron::hours(6)); } - SaveRetryTime(chron::system_clock::now()+std::chrono::duration_cast(delay)); + SaveRetryTime(chron::system_clock::now() + std::chrono::duration_cast(delay)); clock::time_point myRetryTime = clock::now() + delay; - if (this->updateRetryTime < myRetryTime) + if (this->updateRetryTime < myRetryTime) { this->updateRetryTime = myRetryTime; } } -void Updater::SaveRetryTime(const std::chrono::system_clock::time_point &time) +void UpdaterImpl::SaveRetryTime(const std::chrono::system_clock::time_point &time) { fs::path retryTimePath = WORKING_DIRECTORY / "retryTime.json"; - pipedal::ofstream_synced f {retryTimePath}; + pipedal::ofstream_synced f{retryTimePath}; if (f.is_open()) { json_writer writer(f); auto rep = time.time_since_epoch().count(); writer.write(rep); } - } -Updater::clock::time_point Updater::LoadRetryTime() +UpdaterImpl::clock::time_point UpdaterImpl::LoadRetryTime() { using namespace std::chrono; fs::path retryTimePath = WORKING_DIRECTORY / "retryTime.json"; - std::ifstream f {retryTimePath}; + std::ifstream f{retryTimePath}; if (f.is_open()) { system_clock::duration::rep tRep = 0; @@ -1126,13 +1249,11 @@ Updater::clock::time_point Updater::LoadRetryTime() clockDuration = duration_cast(hours(6)); } return clock::now() + clockDuration; - } return clock::now(); - } -void Updater::DownloadUpdate(const std::string &url, std::filesystem::path *file, std::filesystem::path *signatureFile) +void UpdaterImpl::DownloadUpdate(const std::string &url, std::filesystem::path *file, std::filesystem::path *signatureFile) { std::string filename, signatureUrl; { diff --git a/src/Updater.hpp b/src/Updater.hpp index 38e5c44..f5d7cdf 100644 --- a/src/Updater.hpp +++ b/src/Updater.hpp @@ -26,146 +26,42 @@ #include #include "json.hpp" #include +#include "UpdaterStatus.hpp" -class GithubRelease; namespace pipedal { - class Updater; + class UpdaterImpl; + class GithubRelease; - enum class UpdatePolicyT + + class Updater { - // ordinal values must not change, as files depend on them. - // must match declaration in Updater.tsx - - ReleaseOnly = 0, - ReleaseOrBeta = 1, - Development = 2, - Disabled = 3 - - }; - class UpdateRelease - { - private: - friend class Updater; - bool updateAvailable_ = false; - std::string upgradeVersion_; // just the version. - std::string upgradeVersionDisplayName_; // display name for the version. - std::string assetName_; // filename only - std::string updateUrl_; // url from which to download the .deb file. - std::string gpgSignatureUrl_; // url from which to download the .deb.asc file. + protected: + Updater() {}; public: - UpdateRelease(); + virtual ~Updater() noexcept {} - bool UpdateAvailable() const { return updateAvailable_; } - const std::string &UpgradeVersion() const { return upgradeVersion_; } - const std::string &UpgradeVersionDisplayName() const { return upgradeVersionDisplayName_; } - const std::string &AssetName() const { return assetName_; } - const std::string &UpdateUrl() const { return updateUrl_; } - const std::string &GpgSignatureUrl() const { return gpgSignatureUrl_;} - bool operator==(const UpdateRelease &other) const; - DECLARE_JSON_MAP(UpdateRelease); - }; - - class UpdateStatus - { - private: - friend class Updater; - std::chrono::system_clock::time_point::rep lastUpdateTime_ = 0; - - bool isValid_ = false; - std::string errorMessage_; - bool isOnline_ = false; - std::string currentVersion_; - std::string currentVersionDisplayName_; - - int32_t updatePolicy_ = (int32_t)(UpdatePolicyT::ReleaseOrBeta); - UpdateRelease releaseOnlyRelease_; - UpdateRelease releaseOrBetaRelease_; - UpdateRelease devRelease_; - - public: - UpdateStatus(); - std::chrono::system_clock::time_point LastUpdateTime() const; - void LastUpdateTime(const std::chrono::system_clock::time_point &timePoint); - - void ResetCurrentVersion(); - bool IsValid() const { return isValid_; } - const std::string &ErrorMessage() const { return errorMessage_; } - bool IsOnline() const { return isOnline_; } - const std::string &CurrentVersion() const { return currentVersion_; } - const std::string &CurrentDisplayVersion() const { return currentVersionDisplayName_; } - UpdatePolicyT UpdatePolicy() const { return (UpdatePolicyT)updatePolicy_; } - void UpdatePolicy(UpdatePolicyT updatePreference) { this->updatePolicy_ = (int32_t)updatePreference; } - - const UpdateRelease &ReleaseOnlyRelease() const { return releaseOnlyRelease_; } - const UpdateRelease &ReleaseOrBetaRelease() const { return releaseOrBetaRelease_; } - const UpdateRelease &DevRelease() const { return devRelease_; } - bool operator==(const UpdateStatus &other) const; - - DECLARE_JSON_MAP(UpdateStatus); - }; - - class Updater - { - public: - Updater(); - ~Updater(); + using self = Updater; + using ptr = std::unique_ptr; + static Updater::ptr Create(); static void ValidateSignature(const std::filesystem::path&file, const std::filesystem::path&signatureFile); using UpdateListener = std::function; + virtual void SetUpdateListener(UpdateListener &&listener) = 0; + virtual void CheckNow() = 0; + virtual void Stop() = 0; - void SetUpdateListener(UpdateListener &&listener); - void CheckNow(); - void Stop(); + virtual UpdatePolicyT GetUpdatePolicy() = 0; + virtual void SetUpdatePolicy(UpdatePolicyT updatePolicy) = 0; + virtual void ForceUpdateCheck() = 0; + virtual void DownloadUpdate(const std::string &url, std::filesystem::path*file, std::filesystem::path*signatureFile) = 0; + UpdateStatus virtual GetCurrentStatus() const = 0; - UpdatePolicyT GetUpdatePolicy(); - void SetUpdatePolicy(UpdatePolicyT updatePolicy); - void ForceUpdateCheck(); - void DownloadUpdate(const std::string &url, std::filesystem::path*file, std::filesystem::path*signatureFile); - UpdateStatus GetCurrentStatus() const { return this->currentResult; } - private: - using clock = std::chrono::steady_clock; - void RetryAfter(clock::duration delay); - - template - void RetryAfter(std::chrono::duration duration) - { - RetryAfter(std::chrono::duration_cast(duration)); - } - - void SaveRetryTime(const std::chrono::system_clock::time_point &time); - clock::time_point LoadRetryTime(); - - std::string GetUpdateFilename(const std::string &url); - std::string GetSignatureUrl(const std::string &url); - - UpdatePolicyT updatePolicy = UpdatePolicyT::ReleaseOrBeta; - using UpdateReleasePredicate = std::function; - - UpdateRelease getUpdateRelease( - const std::vector &githubReleases, - const std::string ¤tVersion, - const UpdateReleasePredicate &predicate); - - UpdateStatus cachedUpdateStatus; - bool stopped = false; - - int event_reader = -1; - int event_writer = -1; - void ThreadProc(); - void CheckForUpdate(bool useCache); - UpdateListener listener; - - std::unique_ptr thread; - std::mutex mutex; - - bool hasInfo = false; - UpdateStatus currentResult; - static clock::duration updateRate; - clock::time_point updateRetryTime; }; + + } \ No newline at end of file diff --git a/src/UpdaterStatus.hpp b/src/UpdaterStatus.hpp new file mode 100644 index 0000000..94fab81 --- /dev/null +++ b/src/UpdaterStatus.hpp @@ -0,0 +1,98 @@ +// Copyright (c) 2024 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#pragma once + +#include "json.hpp" + +namespace pipedal { + enum class UpdatePolicyT + { + // ordinal values must not change, as files depend on them. + // must match declaration in Updater.tsx + + ReleaseOnly = 0, + ReleaseOrBeta = 1, + Development = 2, + Disabled = 3 + + }; + class UpdateRelease + { + private: + friend class UpdaterImpl; + bool updateAvailable_ = false; + std::string upgradeVersion_; // just the version. + std::string upgradeVersionDisplayName_; // display name for the version. + std::string assetName_; // filename only + std::string updateUrl_; // url from which to download the .deb file. + std::string gpgSignatureUrl_; // url from which to download the .deb.asc file. + public: + UpdateRelease(); + + bool UpdateAvailable() const { return updateAvailable_; } + const std::string &UpgradeVersion() const { return upgradeVersion_; } + const std::string &UpgradeVersionDisplayName() const { return upgradeVersionDisplayName_; } + const std::string &AssetName() const { return assetName_; } + const std::string &UpdateUrl() const { return updateUrl_; } + const std::string &GpgSignatureUrl() const { return gpgSignatureUrl_;} + bool operator==(const UpdateRelease &other) const; + DECLARE_JSON_MAP(UpdateRelease); + }; + + class UpdateStatus + { + private: + friend class UpdaterImpl; + std::chrono::system_clock::time_point::rep lastUpdateTime_ = 0; + + bool isValid_ = false; + std::string errorMessage_; + bool isOnline_ = false; + std::string currentVersion_; + std::string currentVersionDisplayName_; + + int32_t updatePolicy_ = (int32_t)(UpdatePolicyT::ReleaseOrBeta); + UpdateRelease releaseOnlyRelease_; + UpdateRelease releaseOrBetaRelease_; + UpdateRelease devRelease_; + + public: + UpdateStatus(); + std::chrono::system_clock::time_point LastUpdateTime() const; + void LastUpdateTime(const std::chrono::system_clock::time_point &timePoint); + + void ResetCurrentVersion(); + bool IsValid() const { return isValid_; } + const std::string &ErrorMessage() const { return errorMessage_; } + bool IsOnline() const { return isOnline_; } + const std::string &CurrentVersion() const { return currentVersion_; } + const std::string &CurrentDisplayVersion() const { return currentVersionDisplayName_; } + UpdatePolicyT UpdatePolicy() const { return (UpdatePolicyT)updatePolicy_; } + void UpdatePolicy(UpdatePolicyT updatePreference) { this->updatePolicy_ = (int32_t)updatePreference; } + + const UpdateRelease &ReleaseOnlyRelease() const { return releaseOnlyRelease_; } + const UpdateRelease &ReleaseOrBetaRelease() const { return releaseOrBetaRelease_; } + const UpdateRelease &DevRelease() const { return devRelease_; } + bool operator==(const UpdateStatus &other) const; + + DECLARE_JSON_MAP(UpdateStatus); + }; + +} \ No newline at end of file diff --git a/src/UpdaterTest.cpp b/src/UpdaterTest.cpp index 7262ae9..94113bb 100644 --- a/src/UpdaterTest.cpp +++ b/src/UpdaterTest.cpp @@ -29,10 +29,10 @@ using namespace pipedal; TEST_CASE( "updater test", "[updater]" ) { int nCalls = 0; cout << "------ upater test ----" << endl; - { Updater updater; + { Updater::ptr updater = Updater::Create(); - updater.SetUpdateListener( + updater->SetUpdateListener( [&nCalls](const UpdateStatus&updateStatus) mutable { diff --git a/todo.txt b/todo.txt index 2268cc5..528828a 100644 --- a/todo.txt +++ b/todo.txt @@ -1,7 +1,4 @@ -- Shutdown/reboot midi bindings -- Midi binding for hotspotManager. - - verify that scanning doesn't cause overruns. - address change handlers in client (and browser?) - Keybaord escape for hotspotManager.