diff --git a/.vscode/launch.json b/.vscode/launch.json index 3e7d49c..1bc3a2b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -92,6 +92,31 @@ } ] }, + { + "name": "(gdb) downloadCounts", + "type": "cppdbg", + "request": "launch", + // Resolved by CMake Tools: + "program": "${command:cmake.launchTargetPath}", + "args": [ "--downloads" ], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/../..", + "environment": [ + { + "name": "PATH", + "value": "$PATH:${command:cmake.launchTargetDirectory}" + } + ], + "externalConsole": true, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, { "name": "(gdb) pipedal_nm_p2pd", diff --git a/downloadCounts.sh b/downloadCounts.sh new file mode 100755 index 0000000..34b548d --- /dev/null +++ b/downloadCounts.sh @@ -0,0 +1,2 @@ +#!/usr/bin/bash +./build/src/makeRelease --downloads diff --git a/plotDownloads.sh b/plotDownloads.sh new file mode 100755 index 0000000..0c9937d --- /dev/null +++ b/plotDownloads.sh @@ -0,0 +1,45 @@ +#!/usr/bin/bash +#!/bin/bash + +# Create a temporary file to store the data +temp_file=$(mktemp) + +# Create a temporary file to store the data +temp_file=$(mktemp) + +# Run get_counts and process its output +./build/src/makeRelease --gplot-downloads | while read -r date count; do + # Convert ISO8601 date to timestamp for gnuplot + timestamp=$(date -d "$date" "+%s") + echo "$timestamp $count" >> "$temp_file" +done + +# Create and execute the gnuplot script +gnuplot -persist << EOF +set terminal qt enhanced font "Piboto,12" size 800,600 +set xdata time +set timefmt "%s" +set format x "%Y-%m-%d" +set xlabel "" +set ylabel "Cumulative Downloads" +set title "PiPedal Downloads" font "Piboto:Bold,16" +set grid + +# Rotate x-axis labels by 45 degrees +set xtics rotate by 90 offset 0,-4 + +# Adjust the bottom margin to make room for rotated labels +set bmargin 10 + +unset key + +plot "$temp_file" using 1:2 with lines title "Cumulative downloads" + + +pause mouse close +EOF + + +# Clean up the temporary file +rm "$temp_file" + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e0f61b3..95eb98d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -152,6 +152,7 @@ set (PIPEDAL_SOURCES UpdateResults.cpp UpdateResults.hpp UpdaterSecurity.hpp Updater.cpp Updater.hpp UpdaterStatus.hpp + GithubResponseHeaders.hpp Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp WebServerConfig.cpp WebServerConfig.hpp Locale.hpp Locale.cpp diff --git a/src/GithubResponseHeaders.hpp b/src/GithubResponseHeaders.hpp new file mode 100644 index 0000000..52ad830 --- /dev/null +++ b/src/GithubResponseHeaders.hpp @@ -0,0 +1,76 @@ +// 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 +#include +#include +#include "json.hpp" + +#pragma once +namespace pipedal +{ + 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; + + 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; + }; + + + 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(const std::filesystem::path &filename); + void Save(const std::filesystem::path &filename); + DECLARE_JSON_MAP(GithubResponseHeaders); + + private: + static std::filesystem::path FILENAME; + }; +} + diff --git a/src/Updater.cpp b/src/Updater.cpp index f13f15a..0be6353 100644 --- a/src/Updater.cpp +++ b/src/Updater.cpp @@ -48,29 +48,7 @@ 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(const std::filesystem::path &filename); - void Save(const std::filesystem::path &filename); - DECLARE_JSON_MAP(GithubResponseHeaders); - - private: - static std::filesystem::path FILENAME; - }; -} +#include "GithubResponseHeaders.hpp" class pipedal::UpdaterImpl : public Updater { @@ -372,35 +350,6 @@ void UpdaterImpl::ThreadProc() } } -namespace pipedal -{ - 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; - - 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) { auto o = v.as_object(); @@ -425,6 +374,8 @@ GithubRelease::GithubRelease(json_variant &v) this->published_at = o->at("published_at").as_string(); } + + static std::vector split(const std::string &s, char delimiter) { std::vector tokens; diff --git a/src/makeReleaseMain.cpp b/src/makeReleaseMain.cpp index e20bf2c..5d4e629 100644 --- a/src/makeReleaseMain.cpp +++ b/src/makeReleaseMain.cpp @@ -10,8 +10,13 @@ #include "UpdaterSecurity.hpp" #include "Updater.hpp" #include +#include "SysExec.hpp" +#include "TemporaryFile.hpp" +#include "json_variant.hpp" +#include "GithubResponseHeaders.hpp" using namespace std; +using namespace pipedal; namespace fs = std::filesystem; std::string getVersionString() @@ -60,7 +65,6 @@ void SignPackage() throw std::runtime_error(SS("File does not exist: " << packagePath)); } - // sign the package. // gpg --armor --output "$filename".asc -b "$filename" cout << "--------------------------------------------------------------" << endl; @@ -172,12 +176,205 @@ void GenerateUpdateJson() UpdateStatus updateStatus = updater->GetReleaseGeneratorStatus(); updateStatus.AddRelease(packageName); } + +class DownloadCountAsset +{ +public: + DownloadCountAsset(const 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(); + this->downloads = (uint64_t)(o->at("download_count").as_number()); + } + std::string name; + std::string browser_download_url; + std::string updated_at; + uint64_t downloads; +}; +class DownloadCountRelease +{ +public: + DownloadCountRelease(const 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(); + + 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(DownloadCountAsset(el)); + } + this->published_at = o->at("published_at").as_string(); + } + std::string name; + bool draft = false, prerelease = false; + std::vector assets; + std::string published_at; +}; + +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(); + +} + +void GetDownloadCounts(bool gplotDownloads) +{ + // error handling and temporary file use is different enough that it justifies + // not including this code in production code. + + // const std::string responseOption = "-w \"%{response_code}\""; +#ifdef WIN32 + responseOption = "-w \"%%{response_code}\""; // windows shell requires doubling of the %%. +#endif + + TemporaryFile headerFile{"/tmp"}; + std::string args = SS("-s -L " << GITHUB_RELEASES_URL << " -D " << headerFile.str()); + + auto result = sysExecForOutput("curl", args); + if (result.exitCode != EXIT_SUCCESS) + { + throw std::runtime_error("Github APIs not available."); + } + else + { + // hard throttling for github. + + GithubResponseHeaders githubHeaders{headerFile.Path()}; + + if (githubHeaders.code_ != 200) + { + if ( + 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::string strTime = std::ctime(&time); + + throw std::runtime_error(SS("Github API rate limit exceeded. Try again at " << strTime)); + } + } + + if (result.output.length() == 0) + { + throw std::runtime_error("No internet access."); + } + std::stringstream ss(result.output); + json_reader reader(ss); + json_variant vResult(reader); + + if (vResult.is_object()) + { + auto o = vResult.as_object(); + std::string message = "Unknown error."; + if (o->at("message").is_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("Invalid file format error."); + } + else + { + json_variant::array_ptr vArray = vResult.as_array(); + + std::vector releases; + for (size_t i = 0; i < vArray->size(); ++i) + { + auto &el = vArray->at(i); + DownloadCountRelease release{el}; + if (!release.draft) + { + releases.push_back(std::move(release)); + } + } + if (gplotDownloads) + { + std::sort( + releases.begin(), + releases.end(), + [](const DownloadCountRelease &left, const DownloadCountRelease &right) + { + return left.published_at < right.published_at; // date ascending + }); + + for (size_t i = 0; i < releases.size()-1; ++i) + { + releases[i].published_at = releases[i+1].published_at; + } + if (releases.size() >= 1) + { + releases[releases.size()-1].published_at = timePointToISO8601(std::chrono::system_clock::now()); + } + uint64_t cumulativeCount = 0; + for (const auto &release : releases) + { + for (const auto &asset : release.assets) + { + if (asset.name.ends_with(".deb")) + { + cumulativeCount += asset.downloads; + } + } + cout << release.published_at << " " << cumulativeCount << endl; + } + } + else + { + std::sort( + releases.begin(), + releases.end(), + [](const DownloadCountRelease &left, const DownloadCountRelease &right) + { + return left.published_at > right.published_at; // latest date first. + }); + for (const auto &release : releases) + { + cout << release.name << endl; + for (const auto &asset : release.assets) + { + cout << " " << asset.name << ": " << asset.downloads << endl; + } + } + } + } + } +} int main(int argc, char **argv) { CommandLineParser commandLine; + bool getDownloadCounts = false; + bool gPlotDownloads = false; + try { - SignPackage(); + commandLine.AddOption("--downloads", &getDownloadCounts); + commandLine.AddOption("--gplot-downloads", &gPlotDownloads); + commandLine.Parse(argc, (const char **)argv); + if (commandLine.Arguments().size() != 0) + { + throw std::runtime_error("Invalid arguments."); + } + if (getDownloadCounts || gPlotDownloads) + { + GetDownloadCounts(gPlotDownloads); + } + else + { + SignPackage(); + } } catch (const std::exception &e) {