From 9b222e670f37a1503a9b161f54fcf0c0167bda46 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 15 Mar 2024 23:44:37 -0500 Subject: [PATCH 01/18] Initial installer creation using CMake, CPack and WiX Toolset --- CMakeLists.txt | 6 +++++- scwx-qt/scwx-qt.cmake | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 36db8bb6..3e698f73 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,10 @@ cmake_minimum_required(VERSION 3.21) set(PROJECT_NAME supercell-wx) -project(${PROJECT_NAME} C CXX) +project(${PROJECT_NAME} + VERSION 0.4.3 + DESCRIPTION "Supercell Wx is a free, open source advanced weather radar viewer." + HOMEPAGE_URL "https://github.com/dpaulat/supercell-wx" + LANGUAGES C CXX) set(CMAKE_POLICY_DEFAULT_CMP0054 NEW) set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index f444dd77..4ae651d6 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -608,3 +608,20 @@ install(SCRIPT ${deploy_script_qmaplibre_core} install(SCRIPT ${deploy_script_scwx} COMPONENT supercell-wx) + +if (MSVC) + set(CPACK_PACKAGE_NAME "Supercell Wx") + set(CPACK_PACKAGE_VENDOR "Dan Paulat") + set(CPACK_PACKAGE_FILE_NAME "supercell-wx-v${SCWX_VERSION}-windows-x64") + set(CPACK_PACKAGE_INSTALL_DIRECTORY "Supercell Wx") + set(CPACK_PACKAGE_CHECKSUM SHA256) + set(CPACK_RESOURCE_FILE_LICENSE "${SCWX_DIR}/LICENSE.txt") + set(CPACK_GENERATOR WIX) + set(CPACK_PACKAGE_EXECUTABLES "supercell-wx;Supercell Wx") + set(CPACK_WIX_UPGRADE_GUID 36AD0F51-4D4F-4B5D-AB61-94C6B4E4FE1C) + + set(CPACK_INSTALL_CMAKE_PROJECTS + "${CMAKE_CURRENT_BINARY_DIR};${CMAKE_PROJECT_NAME};supercell-wx;/") + + include(CPack) +endif() From 41f449dc1e7c9d4b5ee796aea45c94db4a25d376 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 15 Mar 2024 23:54:36 -0500 Subject: [PATCH 02/18] Add CI step to generate installer --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 167453e0..9d5e9faa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -195,6 +195,20 @@ jobs: ${{ github.workspace }}/build/bin/*.debug ${{ github.workspace }}/build/lib/*.debug + - name: Build Installer (Windows) + if: matrix.os == 'windows-2022' + shell: pwsh + run: | + cd build + cpack + + - name: Upload Installer (Windows) + if: matrix.os == 'windows-2022' + uses: actions/upload-artifact@v4 + with: + name: supercell-wx-installer-${{ matrix.artifact_suffix }} + path: ${{ github.workspace }}/build/supercell-wx-*.msi* + - name: Build AppImage (Linux) if: matrix.os == 'ubuntu-22.04' env: From f8c19bc318f31e37116cf1045b25c9aa3ad06092 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 16 Mar 2024 09:12:54 -0500 Subject: [PATCH 03/18] Import latest WiX template from CMake source --- scwx-qt/wix.template.in | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 scwx-qt/wix.template.in diff --git a/scwx-qt/wix.template.in b/scwx-qt/wix.template.in new file mode 100644 index 00000000..95ba7fa7 --- /dev/null +++ b/scwx-qt/wix.template.in @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + ProductIcon.ico + + + + + + + + + + + + + + + + + + + + From c164e48c23db1f1502b4f402fbd56089f893facb Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 16 Mar 2024 17:20:36 -0500 Subject: [PATCH 04/18] Close Supercell Wx before install --- scwx-qt/scwx-qt.cmake | 2 ++ scwx-qt/wix.template.in | 18 +++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 4ae651d6..79f1444b 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -619,6 +619,8 @@ if (MSVC) set(CPACK_GENERATOR WIX) set(CPACK_PACKAGE_EXECUTABLES "supercell-wx;Supercell Wx") set(CPACK_WIX_UPGRADE_GUID 36AD0F51-4D4F-4B5D-AB61-94C6B4E4FE1C) + set(CPACK_WIX_TEMPLATE "${CMAKE_CURRENT_SOURCE_DIR}/wix.template.in") + set(CPACK_WIX_EXTENSIONS WiXUtilExtension) set(CPACK_INSTALL_CMAKE_PROJECTS "${CMAKE_CURRENT_BINARY_DIR};${CMAKE_PROJECT_NAME};supercell-wx;/") diff --git a/scwx-qt/wix.template.in b/scwx-qt/wix.template.in index 95ba7fa7..ee565b57 100644 --- a/scwx-qt/wix.template.in +++ b/scwx-qt/wix.template.in @@ -2,7 +2,9 @@ - - - - - - - + @@ -47,6 +44,13 @@ + + From 377eac3cb0071e17070e3e2a27aa30639f71bdc0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 17 Mar 2024 00:18:07 -0500 Subject: [PATCH 05/18] Update license year --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index 70848a51..8c9c5fbd 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021 Dan Paulat +Copyright (c) 2021-2024 Dan Paulat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From f9cc055f8e2de252cc97788694a1d299453a7568 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 17 Mar 2024 00:19:08 -0500 Subject: [PATCH 06/18] Replace default icon, banner and dialog with application-specific images --- scwx-qt/res/images/scwx-banner.png | Bin 0 -> 6272 bytes scwx-qt/res/images/scwx-dialog.png | Bin 0 -> 89710 bytes scwx-qt/scwx-qt.cmake | 3 +++ 3 files changed, 3 insertions(+) create mode 100644 scwx-qt/res/images/scwx-banner.png create mode 100644 scwx-qt/res/images/scwx-dialog.png diff --git a/scwx-qt/res/images/scwx-banner.png b/scwx-qt/res/images/scwx-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..68722339757815b3d3e7bf30c27ae03b793dacf1 GIT binary patch literal 6272 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 E0k%RAMgRZ+ literal 0 HcmV?d00001 diff --git a/scwx-qt/res/images/scwx-dialog.png b/scwx-qt/res/images/scwx-dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..781a39b2883ffe90ffa6a2fd51d4637d56e45275 GIT binary patch literal 89710 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 literal 0 HcmV?d00001 diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 79f1444b..154a419f 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -614,11 +614,14 @@ if (MSVC) set(CPACK_PACKAGE_VENDOR "Dan Paulat") set(CPACK_PACKAGE_FILE_NAME "supercell-wx-v${SCWX_VERSION}-windows-x64") set(CPACK_PACKAGE_INSTALL_DIRECTORY "Supercell Wx") + set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/res/icons/scwx-256.ico") set(CPACK_PACKAGE_CHECKSUM SHA256) set(CPACK_RESOURCE_FILE_LICENSE "${SCWX_DIR}/LICENSE.txt") set(CPACK_GENERATOR WIX) set(CPACK_PACKAGE_EXECUTABLES "supercell-wx;Supercell Wx") set(CPACK_WIX_UPGRADE_GUID 36AD0F51-4D4F-4B5D-AB61-94C6B4E4FE1C) + set(CPACK_WIX_UI_BANNER "${CMAKE_CURRENT_SOURCE_DIR}/res/images/scwx-banner.png") + set(CPACK_WIX_UI_DIALOG "${CMAKE_CURRENT_SOURCE_DIR}/res/images/scwx-dialog.png") set(CPACK_WIX_TEMPLATE "${CMAKE_CURRENT_SOURCE_DIR}/wix.template.in") set(CPACK_WIX_EXTENSIONS WiXUtilExtension) From b025b291612686f100a0ee748b01c89470b806ba Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 17 Mar 2024 09:17:16 -0500 Subject: [PATCH 07/18] Add option to launch Supercell Wx after install is complete --- scwx-qt/scwx-qt.cmake | 2 +- scwx-qt/wix.template.in | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 154a419f..3d734a84 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -623,7 +623,7 @@ if (MSVC) set(CPACK_WIX_UI_BANNER "${CMAKE_CURRENT_SOURCE_DIR}/res/images/scwx-banner.png") set(CPACK_WIX_UI_DIALOG "${CMAKE_CURRENT_SOURCE_DIR}/res/images/scwx-dialog.png") set(CPACK_WIX_TEMPLATE "${CMAKE_CURRENT_SOURCE_DIR}/wix.template.in") - set(CPACK_WIX_EXTENSIONS WiXUtilExtension) + set(CPACK_WIX_EXTENSIONS WixUIExtension WiXUtilExtension) set(CPACK_INSTALL_CMAKE_PROJECTS "${CMAKE_CURRENT_BINARY_DIR};${CMAKE_PROJECT_NAME};supercell-wx;/") diff --git a/scwx-qt/wix.template.in b/scwx-qt/wix.template.in index ee565b57..01a9b8df 100644 --- a/scwx-qt/wix.template.in +++ b/scwx-qt/wix.template.in @@ -44,6 +44,13 @@ + + WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed + + + + + + From 8d706c463c5deb9e23a8db56cb4f232ba0bd21c8 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 18 Mar 2024 23:23:09 -0500 Subject: [PATCH 08/18] Add Release Assets to GitHub Release object --- scwx-qt/source/scwx/qt/types/github_types.cpp | 20 ++++++++++++++++++- scwx-qt/source/scwx/qt/types/github_types.hpp | 20 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/types/github_types.cpp b/scwx-qt/source/scwx/qt/types/github_types.cpp index e741c23b..f0f241f1 100644 --- a/scwx-qt/source/scwx/qt/types/github_types.cpp +++ b/scwx-qt/source/scwx/qt/types/github_types.cpp @@ -11,10 +11,25 @@ namespace types namespace gh { +ReleaseAsset tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv) +{ + auto& jo = jv.as_object(); + + ReleaseAsset asset {}; + + // Required parameters + asset.name_ = jo.at("name").as_string(); + asset.contentType_ = jo.at("content_type").as_string(); + asset.browserDownloadUrl_ = jo.at("browser_download_url").as_string(); + + return asset; +} + Release tag_invoke(boost::json::value_to_tag, const boost::json::value& jv) { - auto jo = jv.as_object(); + auto& jo = jv.as_object(); Release release {}; @@ -24,6 +39,9 @@ Release tag_invoke(boost::json::value_to_tag, release.draft_ = jo.at("draft").as_bool(); release.prerelease_ = jo.at("prerelease").as_bool(); + release.assets_ = + boost::json::value_to>(jo.at("assets")); + // Optional parameters if (jo.contains("body")) { diff --git a/scwx-qt/source/scwx/qt/types/github_types.hpp b/scwx-qt/source/scwx/qt/types/github_types.hpp index 829cfd28..b5fcfaee 100644 --- a/scwx-qt/source/scwx/qt/types/github_types.hpp +++ b/scwx-qt/source/scwx/qt/types/github_types.hpp @@ -13,6 +13,18 @@ namespace types namespace gh { +/** + * @brief GitHub Release Asset object + * + * + */ +struct ReleaseAsset +{ + std::string name_ {}; + std::string contentType_ {}; + std::string browserDownloadUrl_ {}; +}; + /** * @brief GitHub Release object * @@ -25,10 +37,14 @@ struct Release std::string body_ {}; bool draft_ {}; bool prerelease_ {}; + + std::vector assets_ {}; }; -Release tag_invoke(boost::json::value_to_tag, - const boost::json::value& jv); +ReleaseAsset tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv); +Release tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv); } // namespace gh } // namespace types From 94726631cb8bb3cec0ec0988e7c4d32010708868 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 21 Mar 2024 22:21:03 -0500 Subject: [PATCH 09/18] Add bytes to string function --- test/source/scwx/util/strings.test.cpp | 29 ++++++++++++ wxdata/include/scwx/util/strings.hpp | 10 ++++ wxdata/source/scwx/util/strings.cpp | 64 ++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/test/source/scwx/util/strings.test.cpp b/test/source/scwx/util/strings.test.cpp index e91c95d9..825b3999 100644 --- a/test/source/scwx/util/strings.test.cpp +++ b/test/source/scwx/util/strings.test.cpp @@ -7,6 +7,35 @@ namespace scwx namespace util { +class BytesToStringTest : + public testing::TestWithParam> +{ +}; + +TEST_P(BytesToStringTest, BytesToString) +{ + auto& [bytes, expected] = GetParam(); + + std::string s = BytesToString(bytes); + + EXPECT_EQ(s, expected); +} + +INSTANTIATE_TEST_SUITE_P(StringsTest, + BytesToStringTest, + testing::Values(std::make_pair(123, "123 bytes"), + std::make_pair(1000, "0.98 KB"), + std::make_pair(1018, "0.99 KB"), + std::make_pair(1024, "1.0 KB"), + std::make_pair(1127, "1.1 KB"), + std::make_pair(1260, "1.23 KB"), + std::make_pair(24012, "23.4 KB"), + std::make_pair(353974, "346 KB"), + std::make_pair(1024000, "0.98 MB"), + std::make_pair(1048576000, "0.98 GB"), + std::make_pair(1073741824000, + "0.98 TB"))); + TEST(StringsTest, ParseTokensColor) { static const std::string line {"Color: red green blue alpha discarded"}; diff --git a/wxdata/include/scwx/util/strings.hpp b/wxdata/include/scwx/util/strings.hpp index ad0d1f9c..e2821331 100644 --- a/wxdata/include/scwx/util/strings.hpp +++ b/wxdata/include/scwx/util/strings.hpp @@ -9,6 +9,16 @@ namespace scwx namespace util { +/** + * @brief Print the number of bytes using a dynamic suffix and limited number of + * decimal points. + * + * @param [in] bytes Number of bytes + * + * @return Human readable size string + */ +std::string BytesToString(std::ptrdiff_t bytes); + /** * @brief Parse a list of tokens from a string * diff --git a/wxdata/source/scwx/util/strings.cpp b/wxdata/source/scwx/util/strings.cpp index 7067d821..d5ac8cfc 100644 --- a/wxdata/source/scwx/util/strings.cpp +++ b/wxdata/source/scwx/util/strings.cpp @@ -4,12 +4,76 @@ #include #include +#include namespace scwx { namespace util { +std::string BytesToString(std::ptrdiff_t bytes) +{ + auto FormatNumber = [](double number) -> std::string + { + int precision; + + // Determine precision + if (number >= 100.0) + { + precision = 0; + } + else if (number >= 10.0) + { + precision = 1; + } + else + { + precision = 2; + } + + // Format the number + std::string formattedNum = fmt::format("{:.{}f}", number, precision); + + // Remove trailing zeroes + std::size_t found = formattedNum.find_last_not_of('0'); + if (found != std::string::npos && formattedNum[found] == '.') + { + // Keep one trailing zero if it's a decimal point + found++; + } + formattedNum.erase(found + 1, std::string::npos); + + return formattedNum; + }; + + // Print with appropriate suffix + if (bytes < 1000) + { + return fmt::format("{} bytes", bytes); + } + + double kilobytes = bytes / 1024.0; + if (kilobytes < 1000.0) + { + return fmt::format("{} KB", FormatNumber(kilobytes)); + } + + double megabytes = kilobytes / 1024.0; + if (megabytes < 1000.0) + { + return fmt::format("{} MB", FormatNumber(megabytes)); + } + + double gigabytes = megabytes / 1024.0; + if (gigabytes < 1000.0) + { + return fmt::format("{} GB", FormatNumber(gigabytes)); + } + + double terabytes = gigabytes / 1024.0; + return fmt::format("{} TB", FormatNumber(terabytes)); +} + std::vector ParseTokens(const std::string& s, std::vector delimiters, std::size_t pos) From 4ac2626b65f8710925b42bd34149d6838d24bd23 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 24 Mar 2024 23:48:35 -0500 Subject: [PATCH 10/18] Download manager implementation --- scwx-qt/scwx-qt.cmake | 8 +- .../scwx/qt/manager/download_manager.cpp | 201 ++++++++++++++++++ .../scwx/qt/manager/download_manager.hpp | 36 ++++ .../scwx/qt/request/download_request.cpp | 57 +++++ .../scwx/qt/request/download_request.hpp | 51 +++++ 5 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/manager/download_manager.cpp create mode 100644 scwx-qt/source/scwx/qt/manager/download_manager.hpp create mode 100644 scwx-qt/source/scwx/qt/request/download_request.cpp create mode 100644 scwx-qt/source/scwx/qt/request/download_request.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 3d734a84..0468e4f6 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -86,6 +86,7 @@ set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp source/scwx/qt/gl/draw/placefile_triangles.cpp source/scwx/qt/gl/draw/rectangle.cpp) set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp + source/scwx/qt/manager/download_manager.hpp source/scwx/qt/manager/font_manager.hpp source/scwx/qt/manager/media_manager.hpp source/scwx/qt/manager/placefile_manager.hpp @@ -98,6 +99,7 @@ set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp source/scwx/qt/manager/timeline_manager.hpp source/scwx/qt/manager/update_manager.hpp) set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp + source/scwx/qt/manager/download_manager.cpp source/scwx/qt/manager/font_manager.cpp source/scwx/qt/manager/media_manager.cpp source/scwx/qt/manager/placefile_manager.cpp @@ -154,8 +156,10 @@ set(SRC_MODEL source/scwx/qt/model/alert_model.cpp source/scwx/qt/model/radar_site_model.cpp source/scwx/qt/model/tree_item.cpp source/scwx/qt/model/tree_model.cpp) -set(HDR_REQUEST source/scwx/qt/request/nexrad_file_request.hpp) -set(SRC_REQUEST source/scwx/qt/request/nexrad_file_request.cpp) +set(HDR_REQUEST source/scwx/qt/request/download_request.hpp + source/scwx/qt/request/nexrad_file_request.hpp) +set(SRC_REQUEST source/scwx/qt/request/download_request.cpp + source/scwx/qt/request/nexrad_file_request.cpp) set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp source/scwx/qt/settings/general_settings.hpp source/scwx/qt/settings/map_settings.hpp diff --git a/scwx-qt/source/scwx/qt/manager/download_manager.cpp b/scwx-qt/source/scwx/qt/manager/download_manager.cpp new file mode 100644 index 00000000..b5d4f30f --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/download_manager.cpp @@ -0,0 +1,201 @@ +#include +#include + +#include + +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +static const std::string logPrefix_ = "scwx::qt::manager::download_manager"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class DownloadManager::Impl +{ +public: + explicit Impl(DownloadManager* self) : self_ {self} {} + + ~Impl() { threadPool_.join(); } + + boost::asio::thread_pool threadPool_ {1u}; + + DownloadManager* self_; +}; + +DownloadManager::DownloadManager() : p(std::make_unique(this)) {} +DownloadManager::~DownloadManager() = default; + +void DownloadManager::Download( + const std::shared_ptr& request) +{ + boost::asio::post( + p->threadPool_, + [=]() + { + // Prepare destination file + const std::filesystem::path& destinationPath = + request->destination_path(); + + if (!destinationPath.has_parent_path()) + { + logger_->error("Destination has no parent path: \"{}\""); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::IOError); + + return; + } + + const std::filesystem::path parentPath = destinationPath.parent_path(); + + // Create directory if it doesn't exist + if (!std::filesystem::exists(parentPath)) + { + if (!std::filesystem::create_directories(parentPath)) + { + logger_->error("Unable to create download directory: \"{}\"", + parentPath.string()); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::IOError); + + return; + } + } + + // Remove file if it exists + if (std::filesystem::exists(destinationPath)) + { + std::error_code error; + if (!std::filesystem::remove(destinationPath, error)) + { + logger_->error( + "Unable to remove existing destination file ({}): \"{}\"", + error.message(), + destinationPath.string()); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::IOError); + + return; + } + } + + // Open file for writing + std::ofstream ofs {destinationPath, + std::ios_base::out | std::ios_base::binary | + std::ios_base::trunc}; + if (!ofs.is_open() || !ofs.good()) + { + logger_->error( + "Unable to open destination file for writing: \"{}\"", + destinationPath.string()); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::IOError); + + return; + } + + // Download file + cpr::Response response = cpr::Get( + cpr::Url {request->url()}, + cpr::ProgressCallback( + [=](cpr::cpr_off_t downloadTotal, + cpr::cpr_off_t downloadNow, + cpr::cpr_off_t /* uploadTotal */, + cpr::cpr_off_t /* uploadNow */, + std::intptr_t /* userdata */) + { + Q_EMIT request->ProgressUpdated(downloadNow, downloadTotal); + return !request->IsCanceled(); + }), + cpr::WriteCallback( + [=, &ofs](std::string data, std::intptr_t /* userdata */) + { + // Write file + ofs << data; + return !request->IsCanceled(); + })); + + bool ofsGood = ofs.good(); + ofs.close(); + + // Handle response + if (response.error.code == cpr::ErrorCode::OK && + !request->IsCanceled() && ofsGood) + { + logger_->info("Download complete: \"{}\"", request->url()); + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::OK); + } + else + { + request::DownloadRequest::CompleteReason reason = + request::DownloadRequest::CompleteReason::IOError; + + if (request->IsCanceled()) + { + logger_->info("Download request cancelled: \"{}\"", + request->url()); + + reason = request::DownloadRequest::CompleteReason::Canceled; + } + else if (response.error.code != cpr::ErrorCode::OK) + { + logger_->error("Error downloading file ({}): \"{}\"", + response.error.message, + request->url()); + + reason = request::DownloadRequest::CompleteReason::RemoteError; + } + else if (!ofsGood) + { + logger_->error("File I/O error: \"{}\"", + destinationPath.string()); + + reason = request::DownloadRequest::CompleteReason::IOError; + } + + std::error_code error; + if (!std::filesystem::remove(destinationPath, error)) + { + logger_->error("Unable to remove destination file: \"{}\", {}", + destinationPath.string(), + error.message()); + } + + Q_EMIT request->RequestComplete(reason); + } + }); +} + +std::shared_ptr DownloadManager::Instance() +{ + static std::weak_ptr downloadManagerReference_ {}; + static std::mutex instanceMutex_ {}; + + std::unique_lock lock(instanceMutex_); + + std::shared_ptr downloadManager = + downloadManagerReference_.lock(); + + if (downloadManager == nullptr) + { + downloadManager = std::make_shared(); + downloadManagerReference_ = downloadManager; + } + + return downloadManager; +} + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/download_manager.hpp b/scwx-qt/source/scwx/qt/manager/download_manager.hpp new file mode 100644 index 00000000..de5b61f8 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/download_manager.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +class DownloadManager : public QObject +{ + Q_OBJECT + +public: + explicit DownloadManager(); + ~DownloadManager(); + + void Download(const std::shared_ptr& request); + + static std::shared_ptr Instance(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/request/download_request.cpp b/scwx-qt/source/scwx/qt/request/download_request.cpp new file mode 100644 index 00000000..56244d7f --- /dev/null +++ b/scwx-qt/source/scwx/qt/request/download_request.cpp @@ -0,0 +1,57 @@ +#include + +namespace scwx +{ +namespace qt +{ +namespace request +{ + +static const std::string logPrefix_ = "scwx::qt::request::download_request"; + +class DownloadRequest::Impl +{ +public: + explicit Impl(const std::string& url, + const std::filesystem::path& destinationPath) : + url_ {url}, destinationPath_ {destinationPath} + { + } + ~Impl() = default; + + const std::string url_; + const std::filesystem::path destinationPath_; + + bool canceled_ = false; +}; + +DownloadRequest::DownloadRequest(const std::string& url, + const std::filesystem::path& destinationPath) : + p(std::make_unique(url, destinationPath)) +{ +} +DownloadRequest::~DownloadRequest() = default; + +const std::string& DownloadRequest::url() const +{ + return p->url_; +} + +const std::filesystem::path& DownloadRequest::destination_path() const +{ + return p->destinationPath_; +} + +void DownloadRequest::Cancel() +{ + p->canceled_ = true; +} + +bool DownloadRequest::IsCanceled() const +{ + return p->canceled_; +} + +} // namespace request +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/request/download_request.hpp b/scwx-qt/source/scwx/qt/request/download_request.hpp new file mode 100644 index 00000000..4b461b8e --- /dev/null +++ b/scwx-qt/source/scwx/qt/request/download_request.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace request +{ + +class DownloadRequest : public QObject +{ + Q_OBJECT + +public: + enum class CompleteReason + { + OK, + Canceled, + IOError, + RemoteError + }; + + explicit DownloadRequest(const std::string& url, + const std::filesystem::path& destinationPath); + ~DownloadRequest(); + + const std::string& url() const; + const std::filesystem::path& destination_path() const; + + void Cancel(); + + bool IsCanceled() const; + +private: + class Impl; + std::unique_ptr p; + +signals: + void ProgressUpdated(std::ptrdiff_t downloadedBytes, + std::ptrdiff_t totalBytes); + void RequestComplete(CompleteReason reason); +}; + +} // namespace request +} // namespace qt +} // namespace scwx From 2f397106f926990967a3f8755541c40b0433d694 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 25 Mar 2024 00:30:28 -0500 Subject: [PATCH 11/18] Add generic progress dialog, and derived download dialog --- scwx-qt/scwx-qt.cmake | 5 + scwx-qt/source/scwx/qt/ui/download_dialog.cpp | 134 ++++++++++++++++++ scwx-qt/source/scwx/qt/ui/download_dialog.hpp | 38 +++++ scwx-qt/source/scwx/qt/ui/progress_dialog.cpp | 66 +++++++++ scwx-qt/source/scwx/qt/ui/progress_dialog.hpp | 46 ++++++ scwx-qt/source/scwx/qt/ui/progress_dialog.ui | 85 +++++++++++ 6 files changed, 374 insertions(+) create mode 100644 scwx-qt/source/scwx/qt/ui/download_dialog.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/download_dialog.hpp create mode 100644 scwx-qt/source/scwx/qt/ui/progress_dialog.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/progress_dialog.hpp create mode 100644 scwx-qt/source/scwx/qt/ui/progress_dialog.ui diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 0468e4f6..eb94efd3 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -221,6 +221,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/animation_dock_widget.hpp source/scwx/qt/ui/collapsible_group.hpp source/scwx/qt/ui/county_dialog.hpp + source/scwx/qt/ui/download_dialog.hpp source/scwx/qt/ui/flow_layout.hpp source/scwx/qt/ui/imgui_debug_dialog.hpp source/scwx/qt/ui/imgui_debug_widget.hpp @@ -232,6 +233,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/open_url_dialog.hpp source/scwx/qt/ui/placefile_dialog.hpp source/scwx/qt/ui/placefile_settings_widget.hpp + source/scwx/qt/ui/progress_dialog.hpp source/scwx/qt/ui/radar_site_dialog.hpp source/scwx/qt/ui/settings_dialog.hpp source/scwx/qt/ui/update_dialog.hpp) @@ -241,6 +243,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/animation_dock_widget.cpp source/scwx/qt/ui/collapsible_group.cpp source/scwx/qt/ui/county_dialog.cpp + source/scwx/qt/ui/download_dialog.cpp source/scwx/qt/ui/flow_layout.cpp source/scwx/qt/ui/imgui_debug_dialog.cpp source/scwx/qt/ui/imgui_debug_widget.cpp @@ -252,6 +255,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/open_url_dialog.cpp source/scwx/qt/ui/placefile_dialog.cpp source/scwx/qt/ui/placefile_settings_widget.cpp + source/scwx/qt/ui/progress_dialog.cpp source/scwx/qt/ui/radar_site_dialog.cpp source/scwx/qt/ui/settings_dialog.cpp source/scwx/qt/ui/update_dialog.cpp) @@ -266,6 +270,7 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/open_url_dialog.ui source/scwx/qt/ui/placefile_dialog.ui source/scwx/qt/ui/placefile_settings_widget.ui + source/scwx/qt/ui/progress_dialog.ui source/scwx/qt/ui/radar_site_dialog.ui source/scwx/qt/ui/settings_dialog.ui source/scwx/qt/ui/update_dialog.ui) diff --git a/scwx-qt/source/scwx/qt/ui/download_dialog.cpp b/scwx-qt/source/scwx/qt/ui/download_dialog.cpp new file mode 100644 index 00000000..30763f4e --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/download_dialog.cpp @@ -0,0 +1,134 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class DownloadDialog::Impl +{ +public: + explicit Impl(DownloadDialog* self) : self_ {self} + { + updateTimer_.setSingleShot(true); + updateTimer_.setInterval(0); + + QObject::connect(&updateTimer_, + &QTimer::timeout, + self_, + [this]() { UpdateProgress(); }); + }; + ~Impl() = default; + + void UpdateProgress(); + + DownloadDialog* self_; + + boost::timer::cpu_timer timer_ {}; + QTimer updateTimer_ {}; + + std::ptrdiff_t downloadedBytes_ {}; + std::ptrdiff_t totalBytes_ {}; +}; + +DownloadDialog::DownloadDialog(QWidget* parent) : + ProgressDialog(parent), p {std::make_unique(this)} +{ + auto buttonBox = button_box(); + buttonBox->setStandardButtons(QDialogButtonBox::StandardButton::Ok | + QDialogButtonBox::StandardButton::Cancel); + buttonBox->button(QDialogButtonBox::StandardButton::Ok) + ->setText("Install Now"); + + SetRange(0, 100); +} + +DownloadDialog::~DownloadDialog() {} + +void DownloadDialog::set_filename(const std::string& filename) +{ + QString label = tr("Downloading %1...").arg(filename.c_str()); + SetTopLabelText(label); +} + +void DownloadDialog::StartDownload() +{ + // Hide the OK button until the download is finished + button_box() + ->button(QDialogButtonBox::StandardButton::Ok) + ->setVisible(false); + + SetBottomLabelText(tr("Waiting for download to begin...")); + p->timer_.start(); + show(); +} + +void DownloadDialog::UpdateProgress(std::ptrdiff_t downloadedBytes, + std::ptrdiff_t totalBytes) +{ + p->downloadedBytes_ = downloadedBytes; + p->totalBytes_ = totalBytes; + + // Use a one-shot timer to trigger an update, preventing multiple updates per + // frame + p->updateTimer_.start(); +} + +void DownloadDialog::FinishDownload() +{ + button_box()->button(QDialogButtonBox::StandardButton::Ok)->setVisible(true); +} + +void DownloadDialog::CancelDownload() +{ + SetValue(0); + SetBottomLabelText(tr("Error occurred while downloading")); +} + +void DownloadDialog::Impl::UpdateProgress() +{ + using namespace std::chrono_literals; + + const std::ptrdiff_t downloadedBytes = downloadedBytes_; + const std::ptrdiff_t totalBytes = totalBytes_; + + const std::chrono::nanoseconds elapsed {timer_.elapsed().wall}; + + const double percentComplete = + (totalBytes > 0.0) ? static_cast(downloadedBytes) / totalBytes : + 0.0; + const int progressValue = static_cast(percentComplete * 100.0); + + self_->SetValue(progressValue); + + const std::chrono::seconds timeRemaining = + (percentComplete > 0.0) ? + std::chrono::duration_cast( + elapsed / percentComplete - elapsed) : + 0s; + const std::chrono::hours hoursRemaining = + std::chrono::duration_cast(timeRemaining); + + const std::string progressText = + fmt::format("{} of {} downloaded ({}:{:%M:%S} remaining)", + util::BytesToString(downloadedBytes), + util::BytesToString(totalBytes), + hoursRemaining.count(), + timeRemaining); + + self_->SetBottomLabelText(QString::fromStdString(progressText)); +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/download_dialog.hpp b/scwx-qt/source/scwx/qt/ui/download_dialog.hpp new file mode 100644 index 00000000..ecf4a0c0 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/download_dialog.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ +class DownloadDialog : public ProgressDialog +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(DownloadDialog) + +public: + explicit DownloadDialog(QWidget* parent = nullptr); + ~DownloadDialog(); + + void set_filename(const std::string& filename); + +public slots: + void StartDownload(); + void UpdateProgress(std::ptrdiff_t downloadedBytes, + std::ptrdiff_t totalBytes); + void FinishDownload(); + void CancelDownload(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/progress_dialog.cpp b/scwx-qt/source/scwx/qt/ui/progress_dialog.cpp new file mode 100644 index 00000000..70486805 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/progress_dialog.cpp @@ -0,0 +1,66 @@ +#include "progress_dialog.hpp" +#include "ui_progress_dialog.h" + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class ProgressDialog::Impl +{ +public: + explicit Impl() = default; + ~Impl() = default; +}; + +ProgressDialog::ProgressDialog(QWidget* parent) : + QDialog(parent), p {std::make_unique()}, ui(new Ui::ProgressDialog) +{ + ui->setupUi(this); +} + +ProgressDialog::~ProgressDialog() +{ + delete ui; +} + +QDialogButtonBox* ProgressDialog::button_box() const +{ + return ui->buttonBox; +} + +void ProgressDialog::SetTopLabelText(const QString& text) +{ + ui->topLabel->setText(text); +} + +void ProgressDialog::SetBottomLabelText(const QString& text) +{ + ui->bottomLabel->setText(text); +} + +void ProgressDialog::SetMinimum(int minimum) +{ + ui->progressBar->setMinimum(minimum); +} + +void ProgressDialog::SetMaximum(int maximum) +{ + ui->progressBar->setMaximum(maximum); +} + +void ProgressDialog::SetRange(int minimum, int maximum) +{ + ui->progressBar->setRange(minimum, maximum); +} + +void ProgressDialog::SetValue(int value) +{ + ui->progressBar->setValue(value); +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/progress_dialog.hpp b/scwx-qt/source/scwx/qt/ui/progress_dialog.hpp new file mode 100644 index 00000000..71e4ac26 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/progress_dialog.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +class QDialogButtonBox; + +namespace Ui +{ +class ProgressDialog; +} + +namespace scwx +{ +namespace qt +{ +namespace ui +{ +class ProgressDialog : public QDialog +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(ProgressDialog) + +public: + explicit ProgressDialog(QWidget* parent = nullptr); + ~ProgressDialog(); + +protected: + QDialogButtonBox* button_box() const; + +public slots: + void SetTopLabelText(const QString& text); + void SetBottomLabelText(const QString& text); + void SetMinimum(int minimum); + void SetMaximum(int maximum); + void SetRange(int minimum, int maximum); + void SetValue(int value); + +private: + class Impl; + std::unique_ptr p; + Ui::ProgressDialog* ui; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/progress_dialog.ui b/scwx-qt/source/scwx/qt/ui/progress_dialog.ui new file mode 100644 index 00000000..b9b091b3 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/progress_dialog.ui @@ -0,0 +1,85 @@ + + + ProgressDialog + + + + 0 + 0 + 394 + 116 + + + + Dialog + + + + + + Downloading supercell-wx-v0.4.4-windows-x64.msi... + + + + + + + 24 + + + + + + + 25.3 MB of 69.1 MB downloaded (00:00:04 remaining) + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + ProgressDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ProgressDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From 3ab05a1654e07e3b24659049318e4c911a7e5cba Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 26 Mar 2024 00:13:35 -0500 Subject: [PATCH 12/18] Verify downloaded file against content-md5 response header --- .../scwx/qt/manager/download_manager.cpp | 74 +++++++++++++++-- .../scwx/qt/request/download_request.hpp | 3 +- wxdata/include/scwx/util/digest.hpp | 18 ++++ wxdata/source/scwx/util/digest.cpp | 82 +++++++++++++++++++ wxdata/wxdata.cmake | 6 +- 5 files changed, 171 insertions(+), 12 deletions(-) create mode 100644 wxdata/include/scwx/util/digest.hpp create mode 100644 wxdata/source/scwx/util/digest.cpp diff --git a/scwx-qt/source/scwx/qt/manager/download_manager.cpp b/scwx-qt/source/scwx/qt/manager/download_manager.cpp index b5d4f30f..4724424d 100644 --- a/scwx-qt/source/scwx/qt/manager/download_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/download_manager.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -128,15 +129,9 @@ void DownloadManager::Download( bool ofsGood = ofs.good(); ofs.close(); - // Handle response - if (response.error.code == cpr::ErrorCode::OK && - !request->IsCanceled() && ofsGood) - { - logger_->info("Download complete: \"{}\"", request->url()); - Q_EMIT request->RequestComplete( - request::DownloadRequest::CompleteReason::OK); - } - else + // Handle error response + if (response.error.code != cpr::ErrorCode::OK || + request->IsCanceled() || !ofsGood) { request::DownloadRequest::CompleteReason reason = request::DownloadRequest::CompleteReason::IOError; @@ -173,7 +168,68 @@ void DownloadManager::Download( } Q_EMIT request->RequestComplete(reason); + + return; } + + // Handle response + const auto contentMd5 = response.header.find("content-md5"); + if (contentMd5 != response.header.cend() && + !contentMd5->second.empty()) + { + // Open file for reading + std::ifstream is {destinationPath, + std::ios_base::in | std::ios_base::binary}; + if (!is.is_open() || !is.good()) + { + logger_->error( + "Unable to open destination file for reading: \"{}\"", + destinationPath.string()); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::IOError); + + return; + } + + // Compute MD5 + std::vector digest {}; + if (!util::ComputeDigest(EVP_md5(), is, digest)) + { + logger_->error("Failed to compute MD5: \"{}\"", + destinationPath.string()); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::IOError); + + return; + } + + // Compare calculated MD5 with digest in response header + QByteArray expectedDigestArray = + QByteArray::fromBase64(contentMd5->second.c_str()); + std::vector expectedDigest( + expectedDigestArray.cbegin(), expectedDigestArray.cend()); + + if (digest != expectedDigest) + { + QByteArray calculatedDigest( + reinterpret_cast(digest.data()), digest.size()); + + logger_->error("Digest mismatch: {} != {}", + calculatedDigest.toBase64().toStdString(), + contentMd5->second); + + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::DigestError); + + return; + } + } + + logger_->info("Download complete: \"{}\"", request->url()); + Q_EMIT request->RequestComplete( + request::DownloadRequest::CompleteReason::OK); }); } diff --git a/scwx-qt/source/scwx/qt/request/download_request.hpp b/scwx-qt/source/scwx/qt/request/download_request.hpp index 4b461b8e..aa73bd17 100644 --- a/scwx-qt/source/scwx/qt/request/download_request.hpp +++ b/scwx-qt/source/scwx/qt/request/download_request.hpp @@ -22,7 +22,8 @@ public: OK, Canceled, IOError, - RemoteError + RemoteError, + DigestError }; explicit DownloadRequest(const std::string& url, diff --git a/wxdata/include/scwx/util/digest.hpp b/wxdata/include/scwx/util/digest.hpp new file mode 100644 index 00000000..e0bbc3c9 --- /dev/null +++ b/wxdata/include/scwx/util/digest.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +#include + +namespace scwx +{ +namespace util +{ + +bool ComputeDigest(const EVP_MD* mdtype, + std::istream& is, + std::vector& digest); + +} // namespace util +} // namespace scwx diff --git a/wxdata/source/scwx/util/digest.cpp b/wxdata/source/scwx/util/digest.cpp new file mode 100644 index 00000000..fe0c055d --- /dev/null +++ b/wxdata/source/scwx/util/digest.cpp @@ -0,0 +1,82 @@ +#include +#include + +namespace scwx +{ +namespace util +{ + +static const std::string logPrefix_ = "scwx::util::digest"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +bool ComputeDigest(const EVP_MD* mdtype, + std::istream& is, + std::vector& digest) +{ + int mdsize; + EVP_MD_CTX* mdctx = nullptr; + + digest.clear(); + + if ((mdsize = EVP_MD_get_size(mdtype)) < 1) + { + logger_->error("Invalid digest"); + return false; + } + + if ((mdctx = EVP_MD_CTX_new()) == nullptr) + { + logger_->error("Error allocating a digest context"); + return false; + } + + if (!EVP_DigestInit_ex(mdctx, mdtype, nullptr)) + { + logger_->error("Message digest initialization failed"); + EVP_MD_CTX_free(mdctx); + return false; + } + + is.seekg(0, std::ios_base::end); + const std::size_t streamSize = is.tellg(); + is.seekg(0, std::ios_base::beg); + + std::size_t bytesRead = 0; + std::size_t chunkSize = 4096; + std::string fileData; + fileData.resize(chunkSize); + + while (bytesRead < streamSize) + { + const std::size_t bytesRemaining = streamSize - bytesRead; + const std::size_t readSize = std::min(chunkSize, bytesRemaining); + + is.read(fileData.data(), readSize); + + if (!is.good() || !EVP_DigestUpdate(mdctx, fileData.data(), readSize)) + { + logger_->error("Message digest update failed"); + EVP_MD_CTX_free(mdctx); + return false; + } + + bytesRead += readSize; + } + + digest.resize(mdsize); + + if (!EVP_DigestFinal_ex(mdctx, digest.data(), nullptr)) + { + logger_->error("Message digest finalization failed"); + EVP_MD_CTX_free(mdctx); + digest.clear(); + return false; + } + + EVP_MD_CTX_free(mdctx); + + return true; +} + +} // namespace util +} // namespace scwx diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 122786fd..0ce08cbb 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -67,7 +67,8 @@ set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp source/scwx/provider/nexrad_data_provider.cpp source/scwx/provider/nexrad_data_provider_factory.cpp source/scwx/provider/warnings_provider.cpp) -set(HDR_UTIL include/scwx/util/enum.hpp +set(HDR_UTIL include/scwx/util/digest.hpp + include/scwx/util/enum.hpp include/scwx/util/environment.hpp include/scwx/util/float.hpp include/scwx/util/hash.hpp @@ -80,7 +81,8 @@ set(HDR_UTIL include/scwx/util/enum.hpp include/scwx/util/threads.hpp include/scwx/util/time.hpp include/scwx/util/vectorbuf.hpp) -set(SRC_UTIL source/scwx/util/environment.cpp +set(SRC_UTIL source/scwx/util/digest.cpp + source/scwx/util/environment.cpp source/scwx/util/float.cpp source/scwx/util/hash.cpp source/scwx/util/logger.cpp From 6448826d6088ba875003023c6541b78a922fae39 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 26 Mar 2024 22:48:52 -0500 Subject: [PATCH 13/18] Only emit download progress updates every 100ms --- .../scwx/qt/manager/download_manager.cpp | 83 ++++++++++++------- 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/download_manager.cpp b/scwx-qt/source/scwx/qt/manager/download_manager.cpp index 4724424d..276a3106 100644 --- a/scwx-qt/source/scwx/qt/manager/download_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/download_manager.cpp @@ -105,26 +105,52 @@ void DownloadManager::Download( return; } + std::chrono::system_clock::time_point lastUpdated {}; + cpr::cpr_off_t lastDownloadNow {}; + cpr::cpr_off_t lastDownloadTotal {}; + // Download file - cpr::Response response = cpr::Get( - cpr::Url {request->url()}, - cpr::ProgressCallback( - [=](cpr::cpr_off_t downloadTotal, - cpr::cpr_off_t downloadNow, - cpr::cpr_off_t /* uploadTotal */, - cpr::cpr_off_t /* uploadNow */, - std::intptr_t /* userdata */) - { - Q_EMIT request->ProgressUpdated(downloadNow, downloadTotal); - return !request->IsCanceled(); - }), - cpr::WriteCallback( - [=, &ofs](std::string data, std::intptr_t /* userdata */) - { - // Write file - ofs << data; - return !request->IsCanceled(); - })); + cpr::Response response = + cpr::Get(cpr::Url {request->url()}, + cpr::ProgressCallback( + [&](cpr::cpr_off_t downloadTotal, + cpr::cpr_off_t downloadNow, + cpr::cpr_off_t /* uploadTotal */, + cpr::cpr_off_t /* uploadNow */, + std::intptr_t /* userdata */) + { + using namespace std::chrono_literals; + + std::chrono::system_clock::time_point now = + std::chrono::system_clock::now(); + + // Only emit an update every 100ms + if ((now > lastUpdated + 100ms || + downloadNow == downloadTotal) && + (downloadNow != lastDownloadNow || + downloadTotal != lastDownloadTotal)) + { + logger_->trace("Downloaded: {} / {}", + downloadNow, + downloadTotal); + + Q_EMIT request->ProgressUpdated(downloadNow, + downloadTotal); + + lastUpdated = now; + lastDownloadNow = downloadNow; + lastDownloadTotal = downloadTotal; + } + + return !request->IsCanceled(); + }), + cpr::WriteCallback( + [&](std::string data, std::intptr_t /* userdata */) + { + // Write file + ofs << data; + return !request->IsCanceled(); + })); bool ofsGood = ofs.good(); ofs.close(); @@ -138,14 +164,13 @@ void DownloadManager::Download( if (request->IsCanceled()) { - logger_->info("Download request cancelled: \"{}\"", - request->url()); + logger_->info("Download request cancelled: {}", request->url()); reason = request::DownloadRequest::CompleteReason::Canceled; } else if (response.error.code != cpr::ErrorCode::OK) { - logger_->error("Error downloading file ({}): \"{}\"", + logger_->error("Error downloading file ({}): {}", response.error.message, request->url()); @@ -153,8 +178,7 @@ void DownloadManager::Download( } else if (!ofsGood) { - logger_->error("File I/O error: \"{}\"", - destinationPath.string()); + logger_->error("File I/O error: {}", destinationPath.string()); reason = request::DownloadRequest::CompleteReason::IOError; } @@ -162,7 +186,7 @@ void DownloadManager::Download( std::error_code error; if (!std::filesystem::remove(destinationPath, error)) { - logger_->error("Unable to remove destination file: \"{}\", {}", + logger_->error("Unable to remove destination file: {}, {}", destinationPath.string(), error.message()); } @@ -182,9 +206,8 @@ void DownloadManager::Download( std::ios_base::in | std::ios_base::binary}; if (!is.is_open() || !is.good()) { - logger_->error( - "Unable to open destination file for reading: \"{}\"", - destinationPath.string()); + logger_->error("Unable to open destination file for reading: {}", + destinationPath.string()); Q_EMIT request->RequestComplete( request::DownloadRequest::CompleteReason::IOError); @@ -196,7 +219,7 @@ void DownloadManager::Download( std::vector digest {}; if (!util::ComputeDigest(EVP_md5(), is, digest)) { - logger_->error("Failed to compute MD5: \"{}\"", + logger_->error("Failed to compute MD5: {}", destinationPath.string()); Q_EMIT request->RequestComplete( @@ -227,7 +250,7 @@ void DownloadManager::Download( } } - logger_->info("Download complete: \"{}\"", request->url()); + logger_->info("Download complete: {}", request->url()); Q_EMIT request->RequestComplete( request::DownloadRequest::CompleteReason::OK); }); From 7012040c320f91cd2e1882cbb5c213771538a11c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 26 Mar 2024 22:51:11 -0500 Subject: [PATCH 14/18] Don't use a timer to update the download dialog --- scwx-qt/source/scwx/qt/ui/download_dialog.cpp | 65 +++++-------------- 1 file changed, 18 insertions(+), 47 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/download_dialog.cpp b/scwx-qt/source/scwx/qt/ui/download_dialog.cpp index 30763f4e..235ad9bf 100644 --- a/scwx-qt/source/scwx/qt/ui/download_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/download_dialog.cpp @@ -6,7 +6,6 @@ #include #include #include -#include namespace scwx { @@ -18,31 +17,14 @@ namespace ui class DownloadDialog::Impl { public: - explicit Impl(DownloadDialog* self) : self_ {self} - { - updateTimer_.setSingleShot(true); - updateTimer_.setInterval(0); - - QObject::connect(&updateTimer_, - &QTimer::timeout, - self_, - [this]() { UpdateProgress(); }); - }; + explicit Impl() {}; ~Impl() = default; - void UpdateProgress(); - - DownloadDialog* self_; - boost::timer::cpu_timer timer_ {}; - QTimer updateTimer_ {}; - - std::ptrdiff_t downloadedBytes_ {}; - std::ptrdiff_t totalBytes_ {}; }; DownloadDialog::DownloadDialog(QWidget* parent) : - ProgressDialog(parent), p {std::make_unique(this)} + ProgressDialog(parent), p {std::make_unique()} { auto buttonBox = button_box(); buttonBox->setStandardButtons(QDialogButtonBox::StandardButton::Ok | @@ -50,6 +32,7 @@ DownloadDialog::DownloadDialog(QWidget* parent) : buttonBox->button(QDialogButtonBox::StandardButton::Ok) ->setText("Install Now"); + setWindowTitle(tr("Download File")); SetRange(0, 100); } @@ -68,6 +51,7 @@ void DownloadDialog::StartDownload() ->button(QDialogButtonBox::StandardButton::Ok) ->setVisible(false); + SetValue(0); SetBottomLabelText(tr("Waiting for download to begin...")); p->timer_.start(); show(); @@ -75,41 +59,17 @@ void DownloadDialog::StartDownload() void DownloadDialog::UpdateProgress(std::ptrdiff_t downloadedBytes, std::ptrdiff_t totalBytes) -{ - p->downloadedBytes_ = downloadedBytes; - p->totalBytes_ = totalBytes; - - // Use a one-shot timer to trigger an update, preventing multiple updates per - // frame - p->updateTimer_.start(); -} - -void DownloadDialog::FinishDownload() -{ - button_box()->button(QDialogButtonBox::StandardButton::Ok)->setVisible(true); -} - -void DownloadDialog::CancelDownload() -{ - SetValue(0); - SetBottomLabelText(tr("Error occurred while downloading")); -} - -void DownloadDialog::Impl::UpdateProgress() { using namespace std::chrono_literals; - const std::ptrdiff_t downloadedBytes = downloadedBytes_; - const std::ptrdiff_t totalBytes = totalBytes_; - - const std::chrono::nanoseconds elapsed {timer_.elapsed().wall}; + const std::chrono::nanoseconds elapsed {p->timer_.elapsed().wall}; const double percentComplete = (totalBytes > 0.0) ? static_cast(downloadedBytes) / totalBytes : 0.0; const int progressValue = static_cast(percentComplete * 100.0); - self_->SetValue(progressValue); + SetValue(progressValue); const std::chrono::seconds timeRemaining = (percentComplete > 0.0) ? @@ -126,7 +86,18 @@ void DownloadDialog::Impl::UpdateProgress() hoursRemaining.count(), timeRemaining); - self_->SetBottomLabelText(QString::fromStdString(progressText)); + SetBottomLabelText(QString::fromStdString(progressText)); +} + +void DownloadDialog::FinishDownload() +{ + button_box()->button(QDialogButtonBox::StandardButton::Ok)->setVisible(true); +} + +void DownloadDialog::CancelDownload() +{ + SetValue(0); + SetBottomLabelText(tr("Error occurred while downloading")); } } // namespace ui From df7b50568d337993d6bdc4d48316cff4d974dbd4 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 26 Mar 2024 22:52:03 -0500 Subject: [PATCH 15/18] Install update button --- scwx-qt/source/scwx/qt/ui/update_dialog.cpp | 125 +++++++++++++++++++- scwx-qt/source/scwx/qt/ui/update_dialog.hpp | 10 +- scwx-qt/source/scwx/qt/ui/update_dialog.ui | 7 ++ 3 files changed, 131 insertions(+), 11 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp index 4029fa9a..313fd35a 100644 --- a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp @@ -1,10 +1,15 @@ #include "update_dialog.hpp" #include "ui_update_dialog.h" #include +#include #include +#include +#include #include #include +#include +#include namespace scwx { @@ -13,19 +18,26 @@ namespace qt namespace ui { -class UpdateDialogImpl +static const std::string logPrefix_ = "scwx::qt::ui::update_dialog"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class UpdateDialog::Impl { public: - explicit UpdateDialogImpl() = default; - ~UpdateDialogImpl() = default; + explicit Impl(UpdateDialog* self) : self_ {self} {}; + ~Impl() = default; + + void HandleAsset(const types::gh::ReleaseAsset& asset); + + UpdateDialog* self_; std::string downloadUrl_ {}; + std::string installUrl_ {}; + std::string installFilename_ {}; }; UpdateDialog::UpdateDialog(QWidget* parent) : - QDialog(parent), - p {std::make_unique()}, - ui(new Ui::UpdateDialog) + QDialog(parent), p {std::make_unique(this)}, ui(new Ui::UpdateDialog) { ui->setupUi(this); @@ -37,6 +49,8 @@ UpdateDialog::UpdateDialog(QWidget* parent) : ui->bannerLabel->setFont(titleFont); ui->releaseNotesText->setOpenExternalLinks(true); + + ui->installUpdateButton->setVisible(false); } UpdateDialog::~UpdateDialog() @@ -56,6 +70,25 @@ void UpdateDialog::UpdateReleaseInfo(const std::string& latestVersion, QString::fromStdString(latestRelease.body_)); p->downloadUrl_ = latestRelease.htmlUrl_; + + ui->installUpdateButton->setVisible(false); + + for (auto& asset : latestRelease.assets_) + { + p->HandleAsset(asset); + } +} + +void UpdateDialog::Impl::HandleAsset(const types::gh::ReleaseAsset& asset) +{ +#if defined(_WIN32) + if (asset.name_.ends_with(".msi")) + { + self_->ui->installUpdateButton->setVisible(true); + installUrl_ = asset.browserDownloadUrl_; + installFilename_ = asset.name_; + } +#endif } void UpdateDialog::on_downloadButton_clicked() @@ -66,6 +99,86 @@ void UpdateDialog::on_downloadButton_clicked() } } +void UpdateDialog::on_installUpdateButton_clicked() +{ + if (!p->installUrl_.empty()) + { + ui->installUpdateButton->setEnabled(false); + + std::string destinationPath { + QStandardPaths::writableLocation(QStandardPaths::TempLocation) + .toStdString()}; + + std::shared_ptr request = + std::make_shared( + p->installUrl_, + std::filesystem::path(destinationPath) / p->installFilename_); + + DownloadDialog* downloadDialog = new DownloadDialog(this); + downloadDialog->setAttribute(Qt::WA_DeleteOnClose); + + // Connect request signals + connect(request.get(), + &request::DownloadRequest::ProgressUpdated, + downloadDialog, + &DownloadDialog::UpdateProgress); + connect(request.get(), + &request::DownloadRequest::RequestComplete, + downloadDialog, + [=](request::DownloadRequest::CompleteReason reason) + { + switch (reason) + { + case request::DownloadRequest::CompleteReason::OK: + downloadDialog->FinishDownload(); + break; + + default: + downloadDialog->CancelDownload(); + break; + } + }); + + // Connect dialog signals + connect( + downloadDialog, + &QDialog::accepted, + this, + [=]() + { + std::filesystem::path installerPackage = + request->destination_path(); + installerPackage.make_preferred(); + + logger_->info("Launching application installer: {}", + installerPackage.string()); + + if (!QProcess::startDetached( + "msiexec.exe", + {"/i", QString::fromStdString(installerPackage.string())})) + { + logger_->error("Failed to launch installer"); + } + + ui->installUpdateButton->setEnabled(true); + }); + connect(downloadDialog, + &QDialog::rejected, + this, + [=]() + { + request->Cancel(); + + ui->installUpdateButton->setEnabled(true); + }); + + downloadDialog->set_filename(p->installFilename_); + downloadDialog->StartDownload(); + + manager::DownloadManager::Instance()->Download(request); + } +} + } // namespace ui } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/update_dialog.hpp b/scwx-qt/source/scwx/qt/ui/update_dialog.hpp index 0df39648..fee03e2c 100644 --- a/scwx-qt/source/scwx/qt/ui/update_dialog.hpp +++ b/scwx-qt/source/scwx/qt/ui/update_dialog.hpp @@ -16,11 +16,10 @@ namespace qt namespace ui { -class UpdateDialogImpl; - class UpdateDialog : public QDialog { Q_OBJECT + Q_DISABLE_COPY_MOVE(UpdateDialog) public: explicit UpdateDialog(QWidget* parent = nullptr); @@ -31,11 +30,12 @@ public: private slots: void on_downloadButton_clicked(); + void on_installUpdateButton_clicked(); private: - friend UpdateDialogImpl; - std::unique_ptr p; - Ui::UpdateDialog* ui; + class Impl; + std::unique_ptr p; + Ui::UpdateDialog* ui; }; } // namespace ui diff --git a/scwx-qt/source/scwx/qt/ui/update_dialog.ui b/scwx-qt/source/scwx/qt/ui/update_dialog.ui index 0facca2a..5aa8e054 100644 --- a/scwx-qt/source/scwx/qt/ui/update_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/update_dialog.ui @@ -139,6 +139,13 @@ + + + + Install Update + + + From b2e441dc2e135f49d0b39f32c06050757f3e1d6d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 26 Mar 2024 23:27:20 -0500 Subject: [PATCH 16/18] Make sure the download manager doesn't attempt to destruct immediately after starting download --- scwx-qt/source/scwx/qt/ui/update_dialog.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp index 313fd35a..0e644592 100644 --- a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp @@ -31,6 +31,9 @@ public: UpdateDialog* self_; + std::shared_ptr downloadManager_ { + manager::DownloadManager::Instance()}; + std::string downloadUrl_ {}; std::string installUrl_ {}; std::string installFilename_ {}; @@ -175,7 +178,7 @@ void UpdateDialog::on_installUpdateButton_clicked() downloadDialog->set_filename(p->installFilename_); downloadDialog->StartDownload(); - manager::DownloadManager::Instance()->Download(request); + p->downloadManager_->Download(request); } } From d598631a770296236da601d0525d4f444039fa24 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 26 Mar 2024 23:46:56 -0500 Subject: [PATCH 17/18] Remove temporary installer files on application startup --- scwx-qt/source/scwx/qt/main/main_window.cpp | 10 +++++-- .../source/scwx/qt/manager/update_manager.cpp | 29 +++++++++++++++++++ .../source/scwx/qt/manager/update_manager.hpp | 2 ++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 85cff2fe..3249577a 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -627,9 +627,13 @@ void MainWindowImpl::AsyncSetup() // Check for updates if (generalSettings.update_notifications_enabled().GetValue()) { - boost::asio::post( - threadPool_, - [this]() { updateManager_->CheckForUpdates(main::kVersionString_); }); + boost::asio::post(threadPool_, + [this]() + { + manager::UpdateManager::RemoveTemporaryReleases(); + updateManager_->CheckForUpdates( + main::kVersionString_); + }); } } diff --git a/scwx-qt/source/scwx/qt/manager/update_manager.cpp b/scwx-qt/source/scwx/qt/manager/update_manager.cpp index 08304263..d12874cb 100644 --- a/scwx-qt/source/scwx/qt/manager/update_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/update_manager.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace scwx { @@ -230,6 +231,34 @@ UpdateManager::Impl::FindLatestRelease() return {latestRelease, latestReleaseVersion}; } +void UpdateManager::RemoveTemporaryReleases() +{ +#if defined(_WIN32) + const std::string destination { + QStandardPaths::writableLocation(QStandardPaths::TempLocation) + .toStdString()}; + const std::filesystem::path destinationPath {destination}; + std::filesystem::directory_iterator it {destinationPath}; + + for (auto& file : it) + { + if (file.is_regular_file() && file.path().string().ends_with(".msi") && + file.path().stem().string().starts_with("supercell-wx-")) + { + logger_->info("Removing temporary installer: {}", + file.path().string()); + + std::error_code error; + if (!std::filesystem::remove(file.path(), error)) + { + logger_->warn("Error removing temporary installer: {}", + error.message()); + } + } + } +#endif +} + std::shared_ptr UpdateManager::Instance() { static std::weak_ptr updateManagerReference_ {}; diff --git a/scwx-qt/source/scwx/qt/manager/update_manager.hpp b/scwx-qt/source/scwx/qt/manager/update_manager.hpp index 10a97350..5995c237 100644 --- a/scwx-qt/source/scwx/qt/manager/update_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/update_manager.hpp @@ -27,6 +27,8 @@ public: bool CheckForUpdates(const std::string& currentVersion = {}); + static void RemoveTemporaryReleases(); + static std::shared_ptr Instance(); signals: From 8957abea9a5cfb3be11aaf53b3bbc57d9e32bfb5 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 26 Mar 2024 23:54:14 -0500 Subject: [PATCH 18/18] Update fixes for gcc --- scwx-qt/source/scwx/qt/ui/update_dialog.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp index 0e644592..0ca61a18 100644 --- a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp @@ -91,6 +91,8 @@ void UpdateDialog::Impl::HandleAsset(const types::gh::ReleaseAsset& asset) installUrl_ = asset.browserDownloadUrl_; installFilename_ = asset.name_; } +#else + Q_UNUSED(asset) #endif } @@ -147,7 +149,7 @@ void UpdateDialog::on_installUpdateButton_clicked() downloadDialog, &QDialog::accepted, this, - [=]() + [=, this]() { std::filesystem::path installerPackage = request->destination_path(); @@ -168,7 +170,7 @@ void UpdateDialog::on_installUpdateButton_clicked() connect(downloadDialog, &QDialog::rejected, this, - [=]() + [=, this]() { request->Cancel();