Update dialog
This commit is contained in:
+9
-2
@@ -89,12 +89,17 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
set (USE_SANITIZE False)
|
||||
|
||||
|
||||
if (!ENABLE_VST3)
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wno-psabi")
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wno-psabi -DARCHITECTURE=${CPACK_SYSTEM_NAME}")
|
||||
else()
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-multichar -Wno-psabi")
|
||||
endif()
|
||||
|
||||
EXECUTE_PROCESS( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE )
|
||||
message(STATUS DEBIAN_ARCHITECTURE="${DEBIAN_ARCHITECTURE}")
|
||||
|
||||
add_compile_definitions(DEBIAN_ARCHITECTURE="${DEBIAN_ARCHITECTURE}")
|
||||
|
||||
if (CMAKE_BUILD_TYPE MATCHES Debug)
|
||||
message(STATUS "Debug build")
|
||||
@@ -132,6 +137,7 @@ else()
|
||||
endif()
|
||||
|
||||
set (PIPEDAL_SOURCES
|
||||
Updater.cpp Updater.hpp
|
||||
Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp
|
||||
WebServerConfig.cpp WebServerConfig.hpp
|
||||
Locale.hpp Locale.cpp
|
||||
@@ -278,7 +284,8 @@ add_executable(pipedaltest testMain.cpp
|
||||
|
||||
InvertingMutexTest.cpp
|
||||
jsonTest.cpp
|
||||
|
||||
UpdaterTest.cpp
|
||||
|
||||
utilTest.cpp
|
||||
|
||||
AlsaDriverTest.cpp
|
||||
|
||||
+52
-1
@@ -73,6 +73,11 @@ PiPedalModel::PiPedalModel()
|
||||
#else
|
||||
this->jackServerSettings = this->storage.GetJackServerSettings();
|
||||
#endif
|
||||
updater.SetUpdateListener(
|
||||
[this](const UpdateStatus&updateStatus)
|
||||
{
|
||||
this->OnUpdateStatusChanged(updateStatus);
|
||||
});
|
||||
}
|
||||
|
||||
void PiPedalModel::Close()
|
||||
@@ -386,9 +391,9 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId)
|
||||
|
||||
item->stateUpdateCount(item->stateUpdateCount() + 1);
|
||||
|
||||
IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()];
|
||||
|
||||
Lv2PluginState newState = item->lv2State();
|
||||
IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()];
|
||||
for (size_t i = 0; i < subscribers.size(); ++i)
|
||||
{
|
||||
t[i] = this->subscribers[i];
|
||||
@@ -2172,3 +2177,49 @@ void PiPedalModel::SetRestartListener(std::function<void(void)> &&listener)
|
||||
{
|
||||
this->restartListener = std::move(listener);
|
||||
}
|
||||
|
||||
void PiPedalModel::OnUpdateStatusChanged(const UpdateStatus&updateStatus)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
|
||||
if (this->currentUpdateStatus != updateStatus)
|
||||
{
|
||||
this->currentUpdateStatus = updateStatus;
|
||||
FireUpdateStatusChanged(this->currentUpdateStatus);
|
||||
}
|
||||
}
|
||||
void PiPedalModel::FireUpdateStatusChanged(const UpdateStatus&updateStatus)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
|
||||
std::vector<IPiPedalModelSubscriber *> t;
|
||||
t.reserve(subscribers.size());
|
||||
|
||||
for (auto subscriber: subscribers)
|
||||
{
|
||||
t.push_back(subscriber);
|
||||
}
|
||||
|
||||
for (auto subscriber: t)
|
||||
{
|
||||
subscriber->OnUpdateStatusChanged(updateStatus);
|
||||
}
|
||||
}
|
||||
UpdateStatus PiPedalModel::GetUpdateStatus() {
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
return currentUpdateStatus;
|
||||
}
|
||||
|
||||
void PiPedalModel::UpdateNow(const std::string&updateUrl)
|
||||
{
|
||||
sleep(5);
|
||||
throw std::runtime_error("Not implemented yet.");
|
||||
|
||||
}
|
||||
void PiPedalModel::ForceUpdateCheck() {
|
||||
updater.ForceUpdateCheck();
|
||||
}
|
||||
void PiPedalModel::SetUpdatePolicy(UpdatePolicyT updatePolicy)
|
||||
{
|
||||
updater.SetUpdatePolicy(updatePolicy);
|
||||
}
|
||||
|
||||
+11
-2
@@ -30,6 +30,7 @@
|
||||
#include <functional>
|
||||
#include <filesystem>
|
||||
#include "Banks.hpp"
|
||||
#include "Updater.hpp"
|
||||
#include "PiPedalConfiguration.hpp"
|
||||
#include "JackServerSettings.hpp"
|
||||
#include "WifiConfigSettings.hpp"
|
||||
@@ -56,7 +57,7 @@ namespace pipedal
|
||||
virtual void OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0;
|
||||
virtual void OnInputVolumeChanged(float value) = 0;
|
||||
virtual void OnOutputVolumeChanged(float value) = 0;
|
||||
|
||||
virtual void OnUpdateStatusChanged(const UpdateStatus&updateStatus) = 0;
|
||||
virtual void OnLv2StateChanged(int64_t pedalItemId,const Lv2PluginState&newState ) = 0;
|
||||
virtual void OnVst3ControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value, const std::string &state) = 0;
|
||||
virtual void OnPedalboardChanged(int64_t clientId, const Pedalboard &pedalboard) = 0;
|
||||
@@ -86,6 +87,10 @@ namespace pipedal
|
||||
class PiPedalModel : private IAudioHostCallbacks
|
||||
{
|
||||
private:
|
||||
|
||||
UpdateStatus currentUpdateStatus;
|
||||
Updater updater;
|
||||
void OnUpdateStatusChanged(const UpdateStatus&updateStatus);
|
||||
std::function<void(void)> restartListener;
|
||||
|
||||
std::unique_ptr<Lv2PluginChangeMonitor> pluginChangeMonitor;
|
||||
@@ -199,6 +204,9 @@ namespace pipedal
|
||||
PiPedalModel();
|
||||
virtual ~PiPedalModel();
|
||||
|
||||
UpdateStatus GetUpdateStatus();
|
||||
void UpdateNow(const std::string&updateUrl);
|
||||
void FireUpdateStatusChanged(const UpdateStatus&updateStatus);
|
||||
uint16_t GetWebPort() const { return webPort; }
|
||||
std::filesystem::path GetPluginUploadDirectory() const;
|
||||
void Close();
|
||||
@@ -334,7 +342,8 @@ namespace pipedal
|
||||
|
||||
std::map<std::string, bool> GetFavorites() const;
|
||||
void SetFavorites(const std::map<std::string, bool> &favorites);
|
||||
|
||||
void SetUpdatePolicy(UpdatePolicyT updatePolicy);
|
||||
void ForceUpdateCheck();
|
||||
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
||||
std::vector<FileEntry> GetFileList2(const std::string &relativePath,const UiFileProperty&fileProperty);
|
||||
|
||||
|
||||
+30
-1
@@ -20,6 +20,7 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include "PiPedalSocket.hpp"
|
||||
#include "Updater.hpp"
|
||||
#include "json.hpp"
|
||||
#include "viewstream.hpp"
|
||||
#include "PiPedalVersion.hpp"
|
||||
@@ -1001,7 +1002,19 @@ public:
|
||||
pReader->read(&handle);
|
||||
this->model.CancelMonitorPatchProperty(this->clientId, handle);
|
||||
}
|
||||
|
||||
else if (message == "getUpdateStatus")
|
||||
{
|
||||
UpdateStatus updateStatus = model.GetUpdateStatus();
|
||||
this->Reply(replyTo,"getUpdateStatus",updateStatus);
|
||||
}
|
||||
else if (message == "updateNow")
|
||||
{
|
||||
std::string updateUrl;
|
||||
pReader->read(&updateUrl);
|
||||
model.UpdateNow(updateUrl);
|
||||
bool result = true;
|
||||
this->Reply(replyTo,"updateNow",result);
|
||||
}
|
||||
else if (message == "getJackStatus")
|
||||
{
|
||||
JackHostStatus status = model.GetJackStatus();
|
||||
@@ -1442,6 +1455,18 @@ public:
|
||||
pReader->read(&favorites);
|
||||
this->model.SetFavorites(favorites);
|
||||
}
|
||||
else if (message == "setUpdatePolicy")
|
||||
{
|
||||
int iPolicy;
|
||||
pReader->read(&iPolicy);
|
||||
|
||||
this->model.SetUpdatePolicy((UpdatePolicyT)iPolicy);
|
||||
}
|
||||
else if (message == "forceUpdateCheck")
|
||||
{
|
||||
this->model.ForceUpdateCheck();
|
||||
|
||||
}
|
||||
else if (message == "setSystemMidiBindings")
|
||||
{
|
||||
std::vector<MidiBinding> bindings;
|
||||
@@ -1581,6 +1606,10 @@ protected:
|
||||
}
|
||||
|
||||
private:
|
||||
virtual void OnUpdateStatusChanged(const UpdateStatus&updateStatus) {
|
||||
Send("onUpdateStatusChanged",updateStatus);
|
||||
}
|
||||
|
||||
virtual void OnLv2StateChanged(int64_t instanceId, const Lv2PluginState &state)
|
||||
{
|
||||
Lv2StateChangedBody message { (uint64_t)instanceId, state};
|
||||
|
||||
+609
@@ -0,0 +1,609 @@
|
||||
// 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.
|
||||
|
||||
#include "Updater.hpp"
|
||||
#include "json.hpp"
|
||||
#include "config.hpp"
|
||||
#include <sys/eventfd.h>
|
||||
#include <unistd.h>
|
||||
#include <stdexcept>
|
||||
#include <chrono>
|
||||
#include <poll.h>
|
||||
#include "Lv2Log.hpp"
|
||||
#include "SysExec.hpp"
|
||||
#include "json_variant.hpp"
|
||||
#include "ss.hpp"
|
||||
#include "Lv2Log.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
#define TEST_UPDATE
|
||||
|
||||
#ifndef DEBUG
|
||||
#undef TEST_UPDATE // do NOT leat this leak into a production build!
|
||||
#endif
|
||||
|
||||
static constexpr uint64_t CLOSE_EVENT = 0;
|
||||
static constexpr uint64_t CHECK_NOW_EVENT = 1;
|
||||
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";
|
||||
|
||||
static std::string GITHUB_RELEASES_URL = "https://api.github.com/repos/rerdavies/pipedal/releases";
|
||||
|
||||
Updater::clock::duration Updater::updateRate = std::chrono::duration_cast<Updater::clock::duration>(std::chrono::days(1));
|
||||
static std::chrono::system_clock::duration CACHE_DURATION = std::chrono::duration_cast<std::chrono::system_clock::duration>(std::chrono::minutes(30));
|
||||
|
||||
std::mutex cacheMutex;
|
||||
static UpdateStatus GetCachedUpdateStatus()
|
||||
{
|
||||
std::lock_guard lock{cacheMutex};
|
||||
try
|
||||
{
|
||||
if (std::filesystem::exists(UPDATE_STATUS_CACHE_FILE))
|
||||
{
|
||||
std::ifstream f{UPDATE_STATUS_CACHE_FILE};
|
||||
if (!f.is_open())
|
||||
{
|
||||
json_reader reader(f);
|
||||
UpdateStatus status;
|
||||
reader.read(&status);
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Unable to read cached UpdateStatus. " << e.what()));
|
||||
}
|
||||
return UpdateStatus();
|
||||
}
|
||||
|
||||
static void SetCachedUpdateStatus(UpdateStatus &updateStatus)
|
||||
{
|
||||
std::lock_guard lock{cacheMutex};
|
||||
updateStatus.LastUpdateTime(std::chrono::system_clock::now());
|
||||
try
|
||||
{
|
||||
std::ofstream f{UPDATE_STATUS_CACHE_FILE};
|
||||
json_writer writer{f};
|
||||
writer.write(updateStatus);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Unable to write cached UpdateStatus. " << e.what()));
|
||||
}
|
||||
}
|
||||
Updater::Updater()
|
||||
{
|
||||
cachedUpdateStatus = GetCachedUpdateStatus();
|
||||
updatePolicy = cachedUpdateStatus.UpdatePolicy();
|
||||
currentResult = cachedUpdateStatus;
|
||||
|
||||
int fds[2];
|
||||
int rc = pipe(fds);
|
||||
if (rc != 0)
|
||||
{
|
||||
throw std::runtime_error("Updater: cant create event pipe.");
|
||||
}
|
||||
this->event_reader = fds[0];
|
||||
this->event_writer = fds[1];
|
||||
|
||||
this->thread = std::make_unique<std::thread>([this]()
|
||||
{ ThreadProc(); });
|
||||
CheckNow();
|
||||
}
|
||||
Updater::~Updater()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
void Updater::Stop()
|
||||
{
|
||||
if (stopped)
|
||||
{
|
||||
return;
|
||||
}
|
||||
stopped = true;
|
||||
|
||||
if (event_writer != -1)
|
||||
{
|
||||
uint64_t value = CLOSE_EVENT;
|
||||
write(this->event_writer, &value, sizeof(uint64_t));
|
||||
}
|
||||
if (thread)
|
||||
{
|
||||
thread->join();
|
||||
thread = nullptr;
|
||||
}
|
||||
if (event_reader != -1)
|
||||
{
|
||||
close(event_reader);
|
||||
}
|
||||
if (event_writer != -1)
|
||||
{
|
||||
close(event_writer);
|
||||
}
|
||||
}
|
||||
|
||||
void Updater::CheckNow()
|
||||
{
|
||||
uint64_t value = CHECK_NOW_EVENT;
|
||||
write(this->event_writer, &value, sizeof(uint64_t));
|
||||
}
|
||||
|
||||
void Updater::SetUpdateListener(UpdateListener &&listener)
|
||||
{
|
||||
std::lock_guard lock{mutex};
|
||||
this->listener = listener;
|
||||
if (hasInfo)
|
||||
{
|
||||
listener(currentResult);
|
||||
}
|
||||
}
|
||||
|
||||
void Updater::ThreadProc()
|
||||
{
|
||||
struct pollfd pfd;
|
||||
pfd.fd = this->event_reader;
|
||||
pfd.events = POLLIN;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int ret = poll(&pfd, 1, std::chrono::duration_cast<std::chrono::milliseconds>(updateRate).count()); // 1000 ms timeout
|
||||
|
||||
if (ret == -1)
|
||||
{
|
||||
Lv2Log::error("Updater: Poll error.");
|
||||
break;
|
||||
}
|
||||
else if (ret == 0)
|
||||
{
|
||||
CheckForUpdate(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Event occurred
|
||||
uint64_t value;
|
||||
ssize_t s = read(event_reader, &value, sizeof(uint64_t));
|
||||
if (s == sizeof(uint64_t))
|
||||
{
|
||||
if (value == CHECK_NOW_EVENT)
|
||||
{
|
||||
CheckForUpdate(true);
|
||||
} else if (value == UNCACHED_CHECK_NOW_EVENT)
|
||||
{
|
||||
CheckForUpdate(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
bool draft = true;
|
||||
bool prerelease = true;
|
||||
std::string name;
|
||||
std::string url;
|
||||
std::string version;
|
||||
std::string body;
|
||||
std::vector<GithubAsset> assets;
|
||||
std::string published_at;
|
||||
};
|
||||
|
||||
GithubAsset::GithubAsset(json_variant &v)
|
||||
{
|
||||
auto o = v.as_object();
|
||||
this->name = o->at("name").as_string();
|
||||
this->browser_download_url = o->at("browser_download_url").as_string();
|
||||
this->updated_at = o->at("updated_at").as_string();
|
||||
}
|
||||
GithubRelease::GithubRelease(json_variant &v)
|
||||
{
|
||||
auto o = v.as_object();
|
||||
this->name = o->at("name").as_string();
|
||||
this->draft = o->at("draft").as_bool();
|
||||
this->prerelease = o->at("prerelease").as_bool();
|
||||
this->body = o->at("body").as_string();
|
||||
|
||||
auto assets = o->at("assets").as_array();
|
||||
for (size_t i = 0; i < assets->size(); ++i)
|
||||
{
|
||||
auto &el = assets->at(i);
|
||||
this->assets.push_back(GithubAsset(el));
|
||||
}
|
||||
this->published_at = o->at("published_at").as_string();
|
||||
}
|
||||
|
||||
static std::vector<std::string> split(const std::string &s, char delimiter)
|
||||
{
|
||||
std::vector<std::string> tokens;
|
||||
std::string token;
|
||||
std::istringstream tokenStream(s);
|
||||
while (std::getline(tokenStream, token, delimiter))
|
||||
{
|
||||
tokens.push_back(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
static std::string justTheVersion(const std::string &assetName)
|
||||
{
|
||||
// eg. pipedal_1.2.41_arm64.deb
|
||||
auto t = split(assetName, '_');
|
||||
if (t.size() != 3)
|
||||
{
|
||||
throw std::runtime_error("Unable to parse version.");
|
||||
}
|
||||
return t[1];
|
||||
}
|
||||
|
||||
int compareVersions(const std::string &l, const std::string &r)
|
||||
{
|
||||
std::stringstream sl(l);
|
||||
std::stringstream sr(r);
|
||||
|
||||
int majorL = -1, majorR = 1, minorL = -1,
|
||||
minorR = -1, buildL = -1, buildR = -1;
|
||||
sl >> majorL;
|
||||
sr >> majorR;
|
||||
if (majorL != majorR)
|
||||
{
|
||||
return (majorL < majorR) ? -1 : 1;
|
||||
}
|
||||
char discard;
|
||||
sl >> discard >> minorL;
|
||||
sr >> discard >> minorR;
|
||||
if (minorL != minorR)
|
||||
{
|
||||
return minorL < minorR ? -1 : 1;
|
||||
}
|
||||
sl >> discard >> buildL;
|
||||
sr >> discard >> buildR;
|
||||
|
||||
if (buildL != buildR)
|
||||
{
|
||||
return buildL < buildR ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static std::string normalizeReleaseName(const std::string &releaseName)
|
||||
{
|
||||
// e.g. "PiPedal 1.2.34 Release" -> "PiPedal v1.2.34-Release"
|
||||
if (releaseName.empty())
|
||||
return "";
|
||||
|
||||
std::string result = releaseName;
|
||||
|
||||
auto nPos = result.find(' ');
|
||||
if (nPos == std::string::npos)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
++nPos;
|
||||
if (nPos >= result.length())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
char c = releaseName[nPos];
|
||||
if (c >= '0' && c <= '9')
|
||||
{
|
||||
result.insert(result.begin() + nPos, 'v');
|
||||
}
|
||||
nPos = result.find(' ', nPos);
|
||||
if (nPos != std::string::npos)
|
||||
{
|
||||
result.at(nPos) = '-';
|
||||
}
|
||||
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(
|
||||
const std::vector<GithubRelease> &githubReleases,
|
||||
const std::string ¤tVersion,
|
||||
const UpdateReleasePredicate &predicate)
|
||||
{
|
||||
for (const auto &githubRelease : githubReleases)
|
||||
{
|
||||
auto *asset = githubRelease.GetDownloadForCurrentArchitecture();
|
||||
if (!asset)
|
||||
continue;
|
||||
|
||||
if (!predicate(githubRelease))
|
||||
continue;
|
||||
UpdateRelease updateRelease;
|
||||
updateRelease.upgradeVersion_ = justTheVersion(asset->name);
|
||||
updateRelease.updateAvailable_ = compareVersions(currentVersion, updateRelease.upgradeVersion_) < 0;
|
||||
updateRelease.upgradeVersionDisplayName_ = normalizeReleaseName(githubRelease.name);
|
||||
updateRelease.assetName_ = asset->name;
|
||||
updateRelease.updateUrl_ = asset->browser_download_url;
|
||||
return updateRelease;
|
||||
}
|
||||
return UpdateRelease();
|
||||
}
|
||||
|
||||
void Updater::CheckForUpdate(bool useCache)
|
||||
{
|
||||
UpdateStatus updateResult;
|
||||
|
||||
{
|
||||
std::lock_guard lock {mutex};
|
||||
if (useCache && IsCacheValid(cachedUpdateStatus))
|
||||
{
|
||||
this->currentResult = cachedUpdateStatus;
|
||||
this->currentResult.UpdatePolicy(this->updatePolicy);
|
||||
if (listener)
|
||||
{
|
||||
listener(this->currentResult);
|
||||
}
|
||||
return;
|
||||
}
|
||||
updateResult = this->currentResult;
|
||||
}
|
||||
std::string args = SS("-s " << GITHUB_RELEASES_URL);
|
||||
|
||||
|
||||
updateResult.errorMessage_ = "";
|
||||
|
||||
try
|
||||
{
|
||||
auto result = sysExecForOutput("curl", args);
|
||||
if (result.exitCode != EXIT_SUCCESS)
|
||||
{
|
||||
throw std::runtime_error("Server has no internet access.");
|
||||
}
|
||||
else
|
||||
{
|
||||
std::stringstream ss(result.output);
|
||||
json_reader reader(ss);
|
||||
json_variant vResult(reader);
|
||||
|
||||
if (vResult.is_object())
|
||||
{
|
||||
// an HTML error.
|
||||
updateResult.isOnline_ = false;
|
||||
auto o = vResult.as_object();
|
||||
std::string message = o->at("message").as_string();
|
||||
auto status_code = o->at("status_code").as_int64();
|
||||
throw std::runtime_error(SS("Service error. ()" << status_code << ": " << message << ")"));
|
||||
}
|
||||
else
|
||||
{
|
||||
json_variant::array_ptr vArray = vResult.as_array();
|
||||
|
||||
std::vector<GithubRelease> releases;
|
||||
for (size_t i = 0; i < vArray->size(); ++i)
|
||||
{
|
||||
auto &el = vArray->at(0);
|
||||
GithubRelease release{el};
|
||||
if (!release.draft && release.GetDownloadForCurrentArchitecture() != nullptr)
|
||||
{
|
||||
releases.push_back(std::move(release));
|
||||
}
|
||||
}
|
||||
std::sort(
|
||||
releases.begin(),
|
||||
releases.end(),
|
||||
[](const GithubRelease &left, const GithubRelease &right)
|
||||
{
|
||||
return left.published_at > right.published_at; // latest date first.
|
||||
});
|
||||
updateResult.releaseOnlyRelease_ = getUpdateRelease(
|
||||
releases,
|
||||
updateResult.currentVersion_,
|
||||
[](const GithubRelease &githubRelease)
|
||||
{
|
||||
return !githubRelease.prerelease &&
|
||||
githubRelease.name.find("Release") != std::string::npos;
|
||||
});
|
||||
|
||||
updateResult.releaseOrBetaRelease_ = getUpdateRelease(
|
||||
releases,
|
||||
updateResult.currentVersion_,
|
||||
[](const GithubRelease &githubRelease)
|
||||
{
|
||||
return !githubRelease.prerelease &&
|
||||
(githubRelease.name.find("Release") != std::string::npos ||
|
||||
githubRelease.name.find("Beta") != std::string::npos);
|
||||
});
|
||||
updateResult.devRelease_ = getUpdateRelease(
|
||||
releases,
|
||||
updateResult.currentVersion_,
|
||||
[](const GithubRelease &githubRelease)
|
||||
{
|
||||
return true;
|
||||
});
|
||||
#ifdef TEST_UPDATE
|
||||
updateResult.releaseOrBetaRelease_.upgradeVersionDisplayName_ = "PiPedal v1.2.41-Beta";
|
||||
updateResult.devRelease_.upgradeVersionDisplayName_ = "PiPedal v1.2.39-Experimental";
|
||||
updateResult.devRelease_.upgradeVersion_ = "1.2.39";
|
||||
updateResult.devRelease_.updateAvailable_ = false;
|
||||
#endif
|
||||
updateResult.isValid_ = true;
|
||||
updateResult.isOnline_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Failed to fetch update info. " << e.what()));
|
||||
updateResult.errorMessage_ = e.what();
|
||||
updateResult.isValid_ = false;
|
||||
updateResult.isOnline_ = false;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock{mutex};
|
||||
updateResult.UpdatePolicy(this->updatePolicy);
|
||||
this->currentResult = updateResult;
|
||||
SetCachedUpdateStatus(this->currentResult);
|
||||
if (listener)
|
||||
{
|
||||
listener(this->currentResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool UpdateRelease::operator==(const UpdateRelease &other) const
|
||||
{
|
||||
return (updateAvailable_ == other.updateAvailable_) &&
|
||||
(upgradeVersion_ == other.upgradeVersion_) &&
|
||||
(upgradeVersionDisplayName_ == other.upgradeVersionDisplayName_) &&
|
||||
(assetName_ == other.assetName_) &&
|
||||
(updateUrl_ == other.updateUrl_);
|
||||
}
|
||||
|
||||
bool UpdateStatus::operator==(const UpdateStatus &other) const
|
||||
{
|
||||
return (lastUpdateTime_ == other.lastUpdateTime_) &&
|
||||
(isValid_ == other.isValid_) &&
|
||||
(errorMessage_ == other.errorMessage_) &&
|
||||
(isOnline_ == other.isOnline_) &&
|
||||
(currentVersion_ == other.currentVersion_) &&
|
||||
(currentVersionDisplayName_ == other.currentVersionDisplayName_) &&
|
||||
(updatePolicy_ == other.updatePolicy_) &&
|
||||
(releaseOnlyRelease_ == other.releaseOnlyRelease_) &&
|
||||
(releaseOrBetaRelease_ == other.releaseOrBetaRelease_) &&
|
||||
(devRelease_ == other.devRelease_);
|
||||
}
|
||||
|
||||
UpdatePolicyT Updater::GetUpdatePolicy()
|
||||
{
|
||||
std::lock_guard lock{mutex};
|
||||
return updatePolicy;
|
||||
}
|
||||
void Updater::SetUpdatePolicy(UpdatePolicyT updatePolicy)
|
||||
{
|
||||
std::lock_guard lock{mutex};
|
||||
if (updatePolicy == this->updatePolicy)
|
||||
return;
|
||||
this->updatePolicy = updatePolicy;
|
||||
if (this->currentResult.UpdatePolicy() != updatePolicy)
|
||||
{
|
||||
this->currentResult.UpdatePolicy(updatePolicy);
|
||||
SetCachedUpdateStatus(this->currentResult);
|
||||
if (listener)
|
||||
{
|
||||
listener(currentResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
void Updater::ForceUpdateCheck()
|
||||
{
|
||||
uint64_t value = UNCACHED_CHECK_NOW_EVENT;
|
||||
write(this->event_writer, &value, sizeof(uint64_t));
|
||||
}
|
||||
|
||||
UpdateStatus::UpdateStatus()
|
||||
{
|
||||
currentVersion_ = PROJECT_VER;
|
||||
currentVersionDisplayName_ = PROJECT_DISPLAY_VERSION;
|
||||
|
||||
#ifdef TEST_UPDATE
|
||||
// uncomment this line to test upgrading.
|
||||
currentVersion_ = "1.2.39";
|
||||
currentVersionDisplayName_ = "PiPedal 1.2.39-Debug";
|
||||
#endif
|
||||
}
|
||||
|
||||
std::chrono::system_clock::time_point UpdateStatus::LastUpdateTime() const
|
||||
{
|
||||
std::chrono::system_clock::duration duration{this->lastUpdateTime_};
|
||||
std::chrono::system_clock::time_point tp{duration};
|
||||
return tp;
|
||||
}
|
||||
|
||||
void UpdateStatus::LastUpdateTime(const std::chrono::system_clock::time_point &timePoint)
|
||||
{
|
||||
this->lastUpdateTime_ = timePoint.time_since_epoch().count();
|
||||
}
|
||||
|
||||
const GithubAsset *GithubRelease::GetDownloadForCurrentArchitecture() const
|
||||
{
|
||||
// deb package names end in {DEBIAN_ARCHITECTURE}.deb
|
||||
// pipedal build gets this value from `dpkg --print-architecture`
|
||||
#ifndef DEBIAN_ARCHITECTURE // deb package names end in {DEBIAN_ARCHITECTURE}.deb
|
||||
#error DEBIAN_ARCHITECTURE not defined
|
||||
#endif
|
||||
std::string downloadEnding = SS("_" << (DEBIAN_ARCHITECTURE) << ".deb");
|
||||
|
||||
for (auto &asset : assets)
|
||||
{
|
||||
if (asset.name.ends_with(downloadEnding))
|
||||
{
|
||||
return &asset;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UpdateRelease::UpdateRelease()
|
||||
{
|
||||
}
|
||||
|
||||
JSON_MAP_BEGIN(UpdateRelease)
|
||||
JSON_MAP_REFERENCE(UpdateRelease, updateAvailable)
|
||||
JSON_MAP_REFERENCE(UpdateRelease, upgradeVersion)
|
||||
JSON_MAP_REFERENCE(UpdateRelease, upgradeVersionDisplayName)
|
||||
JSON_MAP_REFERENCE(UpdateRelease, assetName)
|
||||
JSON_MAP_REFERENCE(UpdateRelease, updateUrl)
|
||||
JSON_MAP_END();
|
||||
|
||||
JSON_MAP_BEGIN(UpdateStatus)
|
||||
JSON_MAP_REFERENCE(UpdateStatus, lastUpdateTime)
|
||||
JSON_MAP_REFERENCE(UpdateStatus, isValid)
|
||||
JSON_MAP_REFERENCE(UpdateStatus, errorMessage)
|
||||
JSON_MAP_REFERENCE(UpdateStatus, isOnline)
|
||||
JSON_MAP_REFERENCE(UpdateStatus, currentVersion)
|
||||
JSON_MAP_REFERENCE(UpdateStatus, currentVersionDisplayName)
|
||||
JSON_MAP_REFERENCE(UpdateStatus, updatePolicy)
|
||||
JSON_MAP_REFERENCE(UpdateStatus, releaseOnlyRelease)
|
||||
JSON_MAP_REFERENCE(UpdateStatus, releaseOrBetaRelease)
|
||||
JSON_MAP_REFERENCE(UpdateStatus, devRelease)
|
||||
JSON_MAP_END();
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
// 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 <string>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include "json.hpp"
|
||||
#include <chrono>
|
||||
|
||||
class GithubRelease;
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
|
||||
class Updater;
|
||||
|
||||
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,
|
||||
};
|
||||
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.
|
||||
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_; }
|
||||
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);
|
||||
|
||||
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 UpdateListener = std::function<void(const UpdateStatus &upateResult)>;
|
||||
|
||||
void SetUpdateListener(UpdateListener &&listener);
|
||||
void CheckNow();
|
||||
void Stop();
|
||||
|
||||
UpdatePolicyT GetUpdatePolicy();
|
||||
void SetUpdatePolicy(UpdatePolicyT updatePolicy);
|
||||
void ForceUpdateCheck();
|
||||
private:
|
||||
UpdatePolicyT updatePolicy = UpdatePolicyT::ReleaseOrBeta;
|
||||
using UpdateReleasePredicate = std::function<bool(const GithubRelease &githubRelease)>;
|
||||
|
||||
UpdateRelease getUpdateRelease(
|
||||
const std::vector<GithubRelease> &githubReleases,
|
||||
const std::string ¤tVersion,
|
||||
const UpdateReleasePredicate &predicate);
|
||||
|
||||
UpdateStatus cachedUpdateStatus;
|
||||
bool stopped = false;
|
||||
using clock = std::chrono::steady_clock;
|
||||
|
||||
int event_reader = -1;
|
||||
int event_writer = -1;
|
||||
void ThreadProc();
|
||||
void CheckForUpdate(bool useCache);
|
||||
UpdateListener listener;
|
||||
|
||||
std::unique_ptr<std::thread> thread;
|
||||
std::mutex mutex;
|
||||
|
||||
bool hasInfo = false;
|
||||
UpdateStatus currentResult;
|
||||
static clock::duration updateRate;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2022 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.
|
||||
|
||||
#include "pch.h"
|
||||
#include "catch.hpp"
|
||||
#include "Updater.hpp"
|
||||
#include "json.hpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
TEST_CASE( "updater test", "[updater]" ) {
|
||||
int nCalls = 0;
|
||||
cout << "------ upater test ----" << endl;
|
||||
{ Updater updater;
|
||||
|
||||
|
||||
updater.SetUpdateListener(
|
||||
[&nCalls](const UpdateStatus&updateStatus) mutable
|
||||
{
|
||||
|
||||
cout << "updateStatus:" << endl;
|
||||
json_writer writer(cout,false);
|
||||
writer.write(updateStatus);
|
||||
cout << endl;
|
||||
++nCalls;
|
||||
}
|
||||
);
|
||||
}
|
||||
REQUIRE(nCalls == 1);
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
+30
-1
@@ -158,11 +158,40 @@ TEST_CASE("json smart ptrs", "[json_smart_ptrs][Build][Dev]")
|
||||
}
|
||||
}
|
||||
|
||||
template <typename U>
|
||||
U & VariantAs(json_variant& v)
|
||||
{
|
||||
throw std::runtime_error("Missing specialization.");
|
||||
}
|
||||
|
||||
template <>
|
||||
inline json_null &VariantAs<json_null>(json_variant& v) { return v.as_null(); }
|
||||
|
||||
template <>
|
||||
inline bool &VariantAs<bool>(json_variant& v) { return v.as_bool(); }
|
||||
|
||||
template <>
|
||||
inline double &VariantAs<double>(json_variant& v) { return v.as_number(); }
|
||||
|
||||
template <>
|
||||
inline std::string &VariantAs<std::string>(json_variant& v) { return v.as_string(); }
|
||||
|
||||
template <>
|
||||
inline std::shared_ptr<json_object> &VariantAs<std::shared_ptr<json_object>>(json_variant& v) { return v.as_object(); }
|
||||
|
||||
template <>
|
||||
inline std::shared_ptr<json_array> &VariantAs<std::shared_ptr<json_array>>(json_variant& v) { return v.as_array(); }
|
||||
|
||||
template <>
|
||||
inline json_variant &VariantAs<json_variant>(json_variant& v) { return v; }
|
||||
|
||||
|
||||
|
||||
template <typename T>
|
||||
void TestVariantRoundTrip(const T &value)
|
||||
{
|
||||
json_variant variant(value);
|
||||
T out = variant.as<T>();
|
||||
T &out = VariantAs<T>(variant);
|
||||
REQUIRE(out == value);
|
||||
|
||||
std::string output;
|
||||
|
||||
Reference in New Issue
Block a user