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