diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index c9ba33133d..88ef12a401 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -107,6 +107,7 @@ + diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index 0efa7e98bf..2cf2377050 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -90,6 +90,9 @@ Header Files + + Header Files + @@ -148,4 +151,4 @@ Source Files - + diff --git a/src/common/msi_to_msix_upgrade_lib/msi_to_msix_upgrade.cpp b/src/common/msi_to_msix_upgrade_lib/msi_to_msix_upgrade.cpp index d48145ca54..d99882ec75 100644 --- a/src/common/msi_to_msix_upgrade_lib/msi_to_msix_upgrade.cpp +++ b/src/common/msi_to_msix_upgrade_lib/msi_to_msix_upgrade.cpp @@ -3,15 +3,21 @@ #include #include +#include #include #include #include +#include +#include + namespace { const wchar_t* POWER_TOYS_UPGRADE_CODE = L"{42B84BF7-5FBF-473B-9C8B-049DC16F7708}"; const wchar_t* DONT_SHOW_AGAIN_RECORD_REGISTRY_PATH = L"delete_previous_powertoys_confirm"; + const wchar_t* USER_AGENT = L"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)"; + const wchar_t* LATEST_RELEASE_ENDPOINT = L"https://api.github.com/repos/microsoft/PowerToys/releases/latest"; } namespace localized_strings @@ -82,3 +88,32 @@ bool uninstall_msi_version(const std::wstring& package_path) } return false; } + +std::future> check_for_new_github_release_async() +{ + try + { + winrt::Windows::Web::Http::HttpClient client; + auto headers = client.DefaultRequestHeaders(); + headers.UserAgent().TryParseAdd(USER_AGENT); + + auto response = co_await client.GetAsync(winrt::Windows::Foundation::Uri{ LATEST_RELEASE_ENDPOINT }); + (void)response.EnsureSuccessStatusCode(); + const auto body = co_await response.Content().ReadAsStringAsync(); + auto json_body = json::JsonValue::Parse(body).GetObjectW(); + auto new_version = json_body.GetNamedString(L"tag_name"); + winrt::Windows::Foundation::Uri release_page_uri{ json_body.GetNamedString(L"html_url") }; + + const auto current_version = get_product_version(); + if (new_version == current_version) + { + co_return std::nullopt; + } + + co_return new_version_download_info{ std::move(release_page_uri), new_version.c_str() }; + } + catch (...) + { + co_return std::nullopt; + } +} \ No newline at end of file diff --git a/src/common/msi_to_msix_upgrade_lib/msi_to_msix_upgrade.h b/src/common/msi_to_msix_upgrade_lib/msi_to_msix_upgrade.h index f6a346ff35..8c70e53de3 100644 --- a/src/common/msi_to_msix_upgrade_lib/msi_to_msix_upgrade.h +++ b/src/common/msi_to_msix_upgrade_lib/msi_to_msix_upgrade.h @@ -2,7 +2,17 @@ #include #include +#include + +#include std::wstring get_msi_package_path(); bool uninstall_msi_version(const std::wstring& package_path); -bool offer_msi_uninstallation(); \ No newline at end of file +bool offer_msi_uninstallation(); + +struct new_version_download_info +{ + winrt::Windows::Foundation::Uri release_page_uri; + std::wstring version_string; +}; +std::future> check_for_new_github_release_async(); diff --git a/src/common/msi_to_msix_upgrade_lib/msi_to_msix_upgrade_lib.vcxproj b/src/common/msi_to_msix_upgrade_lib/msi_to_msix_upgrade_lib.vcxproj index 38e21c2846..f13251a99a 100644 --- a/src/common/msi_to_msix_upgrade_lib/msi_to_msix_upgrade_lib.vcxproj +++ b/src/common/msi_to_msix_upgrade_lib/msi_to_msix_upgrade_lib.vcxproj @@ -97,6 +97,7 @@ stdcpplatest true MultiThreaded + /await %(AdditionalOptions) Windows @@ -134,6 +135,7 @@ stdcpplatest true MultiThreadedDebug + /await %(AdditionalOptions) Windows diff --git a/src/common/settings_helpers.h b/src/common/settings_helpers.h index 9dba9359ca..1678ae883c 100644 --- a/src/common/settings_helpers.h +++ b/src/common/settings_helpers.h @@ -7,6 +7,7 @@ namespace PTSettingsHelper { std::wstring get_module_save_folder_location(std::wstring_view powertoy_name); + std::wstring get_root_save_folder_location(); void save_module_settings(std::wstring_view powertoy_name, json::JsonObject& settings); json::JsonObject load_module_settings(std::wstring_view powertoy_name); diff --git a/src/common/timeutil.h b/src/common/timeutil.h new file mode 100644 index 0000000000..348eb7ab9e --- /dev/null +++ b/src/common/timeutil.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace timeutil +{ + inline std::wstring to_string(const time_t time) + { + return std::to_wstring(static_cast(time)); + } + + inline std::optional from_string(const std::wstring& s) + { + try + { + uint64_t i = std::stoull(s); + return static_cast(i); + } + catch (...) + { + return std::nullopt; + } + } + + inline std::time_t now() + { + return winrt::clock::to_time_t(winrt::clock::now()); + } + + namespace diff + { + inline int64_t in_seconds(const std::time_t to, const std::time_t from) + { + return static_cast(std::difftime(to, from)); + } + + inline int64_t in_minutes(const std::time_t to, const std::time_t from) + { + return static_cast(std::difftime(to, from) / 60); + } + + inline int64_t in_hours(const std::time_t to, const std::time_t from) + { + return static_cast(std::difftime(to, from) / 3600); + } + + inline int64_t in_days(const std::time_t to, const std::time_t from) + { + return static_cast(std::difftime(to, from) / (3600 * 24)); + } + } +} diff --git a/src/runner/main.cpp b/src/runner/main.cpp index badced2b6b..96ceea4fdd 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -16,6 +16,11 @@ #include #include #include +#include + +#include "update_state.h" + +#include #if _DEBUG && _WIN64 #include "unhandled_exception_handler.h" @@ -26,6 +31,8 @@ extern "C" IMAGE_DOS_HEADER __ImageBase; namespace localized_strings { const wchar_t MSI_VERSION_IS_ALREADY_RUNNING[] = L"An older version of PowerToys is already running."; + const wchar_t GITHUB_NEW_VERSION_AVAILABLE_OFFER_VISIT[] = L"An update to PowerToys is available. Visit our GitHub page to get "; + const wchar_t GITHUB_NEW_VERSION_AGREE[] = L"Visit"; } namespace @@ -97,6 +104,46 @@ bool start_msi_uninstallation_sequence() return exit_code == 0; } +std::future check_github_updates() +{ + const auto new_version = co_await check_for_new_github_release_async(); + if (!new_version) + { + co_return; + } + using namespace localized_strings; + + std::wstring contents = GITHUB_NEW_VERSION_AVAILABLE_OFFER_VISIT; + contents += new_version->version_string; + contents += L'.'; + notifications::show_toast_with_activations(contents, {}, { notifications::link_button{ GITHUB_NEW_VERSION_AGREE, new_version->release_page_uri.ToString() } }); +} + +void github_update_checking_worker() +{ + const int64_t update_check_period_minutes = 60 * 24; + + auto state = UpdateState::load(); + for (;;) + { + int64_t sleep_minutes_till_next_update = 0; + if (state.github_update_last_checked_date.has_value()) + { + int64_t last_checked_minutes_ago = timeutil::diff::in_minutes(timeutil::now(), *state.github_update_last_checked_date); + if (last_checked_minutes_ago < 0) + { + last_checked_minutes_ago = update_check_period_minutes; + } + sleep_minutes_till_next_update = max(0, update_check_period_minutes - last_checked_minutes_ago); + } + + std::this_thread::sleep_for(std::chrono::minutes(sleep_minutes_till_next_update)); + + check_github_updates().get(); + state.github_update_last_checked_date.emplace(timeutil::now()); + state.save(); + } +} void alert_already_running() { MessageBoxW(nullptr, @@ -271,12 +318,17 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine try { + std::thread{ [] { + github_update_checking_worker(); + } }.detach(); + if (winstore::running_as_packaged()) { std::thread{ [] { start_msi_uninstallation_sequence(); } }.detach(); } + // Singletons initialization order needs to be preserved, first events and // then modules to guarantee the reverse destruction order. SystemMenuHelperInstace(); diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj index cb6e5166c5..c8b1ca5e5f 100644 --- a/src/runner/runner.vcxproj +++ b/src/runner/runner.vcxproj @@ -21,7 +21,7 @@ - + Application true v142 @@ -64,6 +64,7 @@ pch.h ..\common\inc;..\common\Telemetry;..;..\modules;..\..\deps\cpprestsdk\include;%(AdditionalIncludeDirectories) _UNICODE;UNICODE;%(PreprocessorDefinitions) + /await %(AdditionalOptions) AsInvoker @@ -88,6 +89,7 @@ pch.h ..\common\inc;..\common\Telemetry;..;..\modules;..\..\deps\cpprestsdk\include;%(AdditionalIncludeDirectories) _UNICODE;UNICODE;%(PreprocessorDefinitions) + /await %(AdditionalOptions) true @@ -117,6 +119,7 @@ + @@ -124,6 +127,7 @@ + diff --git a/src/runner/runner.vcxproj.filters b/src/runner/runner.vcxproj.filters index 12279dd485..41c2e61788 100644 --- a/src/runner/runner.vcxproj.filters +++ b/src/runner/runner.vcxproj.filters @@ -39,6 +39,9 @@ Utils + + Utils + @@ -79,6 +82,9 @@ Utils + + Utils + diff --git a/src/runner/update_state.cpp b/src/runner/update_state.cpp new file mode 100644 index 0000000000..97ea677197 --- /dev/null +++ b/src/runner/update_state.cpp @@ -0,0 +1,38 @@ +#include "pch.h" +#include "update_state.h" + +#include +#include +#include + +namespace +{ + const wchar_t PERSISTENT_STATE_FILENAME[] = L"\\update_state.json"; +} + +UpdateState UpdateState::load() +{ + const auto file_name = PTSettingsHelper::get_root_save_folder_location() + PERSISTENT_STATE_FILENAME; + auto json = json::from_file(file_name); + UpdateState state; + + if (!json) + { + return state; + } + + state.github_update_last_checked_date = timeutil::from_string(json->GetNamedString(L"github_update_last_checked_date", L"invalid").c_str()); + + return state; +} + +void UpdateState::save() +{ + json::JsonObject json; + if (github_update_last_checked_date.has_value()) + { + json.SetNamedValue(L"github_update_last_checked_date", json::value(timeutil::to_string(*github_update_last_checked_date))); + } + const auto file_name = PTSettingsHelper::get_root_save_folder_location() + PERSISTENT_STATE_FILENAME; + json::to_file(file_name, json); +} diff --git a/src/runner/update_state.h b/src/runner/update_state.h new file mode 100644 index 0000000000..4431956ed0 --- /dev/null +++ b/src/runner/update_state.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +struct UpdateState +{ + std::optional github_update_last_checked_date; + + static UpdateState load(); + void save(); +}; \ No newline at end of file