/ src / runner / UpdateUtils.cpp
UpdateUtils.cpp
  1  #include "pch.h"
  2  
  3  #include "Generated Files/resource.h"
  4  
  5  #include "ActionRunnerUtils.h"
  6  #include "general_settings.h"
  7  #include "trace.h"
  8  #include "UpdateUtils.h"
  9  
 10  #include <common/utils/gpo.h>
 11  #include <common/logger/logger.h>
 12  #include <common/notifications/notifications.h>
 13  #include <common/updating/installer.h>
 14  #include <common/updating/updating.h>
 15  #include <common/updating/updateState.h>
 16  #include <common/utils/HttpClient.h>
 17  #include <common/utils/process_path.h>
 18  #include <common/utils/resources.h>
 19  #include <common/utils/timeutil.h>
 20  #include <common/version/version.h>
 21  
 22  namespace
 23  {
 24      constexpr int64_t UPDATE_CHECK_INTERVAL_MINUTES = 60 * 24;
 25      constexpr int64_t UPDATE_CHECK_AFTER_FAILED_INTERVAL_MINUTES = 60 * 2;
 26  
 27      // How many minor versions to suspend the toast notification (example: installed=0.60.0, suspend=2, next notification=0.63.*)
 28      // Attention: When changing this value please update the ADML file to.
 29      const int UPDATE_NOTIFICATION_TOAST_SUSPEND_MINOR_VERSION_COUNT = 2;
 30  }
 31  using namespace notifications;
 32  using namespace updating;
 33  
 34  std::wstring CurrentVersionToNextVersion(const new_version_download_info& info)
 35  {
 36      auto result = VersionHelper{ VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION }.toWstring();
 37      result += L" \u2192 "; // Right arrow
 38      result += info.version.toWstring();
 39      return result;
 40  }
 41  
 42  void ShowNewVersionAvailable(const new_version_download_info& info)
 43  {
 44      remove_toasts_by_tag(UPDATING_PROCESS_TOAST_TAG);
 45  
 46      toast_params toast_params{ UPDATING_PROCESS_TOAST_TAG, false };
 47      std::wstring contents = GET_RESOURCE_STRING(IDS_GITHUB_NEW_VERSION_AVAILABLE);
 48      contents += L'\n';
 49      contents += CurrentVersionToNextVersion(info);
 50  
 51      show_toast_with_activations(std::move(contents),
 52                                  GET_RESOURCE_STRING(IDS_TOAST_TITLE),
 53                                  {},
 54                                  { link_button{ GET_RESOURCE_STRING(IDS_GITHUB_NEW_VERSION_UPDATE_NOW),
 55                                                 L"powertoys://update_now/" },
 56                                    link_button{ GET_RESOURCE_STRING(IDS_GITHUB_NEW_VERSION_MORE_INFO),
 57                                                 L"powertoys://open_overview/" } },
 58                                  std::move(toast_params),
 59                                  L"powertoys://open_overview/");
 60  }
 61  
 62  void ShowOpenSettingsForUpdate()
 63  {
 64      remove_toasts_by_tag(UPDATING_PROCESS_TOAST_TAG);
 65  
 66      toast_params toast_params{ UPDATING_PROCESS_TOAST_TAG, false };
 67  
 68      std::vector<action_t> actions = {
 69          link_button{ GET_RESOURCE_STRING(IDS_GITHUB_NEW_VERSION_MORE_INFO),
 70                       L"powertoys://open_overview/" },
 71      };
 72      show_toast_with_activations(GET_RESOURCE_STRING(IDS_GITHUB_NEW_VERSION_AVAILABLE),
 73                                  GET_RESOURCE_STRING(IDS_TOAST_TITLE),
 74                                  {},
 75                                  std::move(actions),
 76                                  std::move(toast_params),
 77                                  L"powertoys://open_overview/");
 78  }
 79  
 80  SHELLEXECUTEINFOW LaunchPowerToysUpdate(const wchar_t* cmdline)
 81  {
 82      std::wstring powertoysUpdaterPath;
 83      powertoysUpdaterPath = get_module_folderpath();
 84  
 85      powertoysUpdaterPath += L"\\PowerToys.Update.exe";
 86      SHELLEXECUTEINFOW sei{ sizeof(sei) };
 87      sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS };
 88      sei.lpFile = powertoysUpdaterPath.c_str();
 89      sei.nShow = SW_SHOWNORMAL;
 90      sei.lpParameters = cmdline;
 91      ShellExecuteExW(&sei);
 92      return sei;
 93  }
 94  
 95  bool IsMeteredConnection()
 96  {
 97      using namespace winrt::Windows::Networking::Connectivity;
 98      ConnectionProfile internetConnectionProfile = NetworkInformation::GetInternetConnectionProfile();
 99      if (!internetConnectionProfile)
100      {
101          return false;
102      }
103  
104      if (internetConnectionProfile.IsWwanConnectionProfile())
105      {
106          return true;
107      }
108  
109      ConnectionCost connectionCost = internetConnectionProfile.GetConnectionCost();
110      if (connectionCost.Roaming()
111          || connectionCost.OverDataLimit()
112          || connectionCost.NetworkCostType() == NetworkCostType::Fixed
113          || connectionCost.NetworkCostType() == NetworkCostType::Variable)
114      {
115          return true;
116      }
117  
118      return false;
119  }
120  
121  void ProcessNewVersionInfo(const github_version_info& version_info,
122                             UpdateState& state,
123                             const bool download_update,
124                             bool show_notifications)
125  {
126      state.githubUpdateLastCheckedDate.emplace(timeutil::now());
127      if (std::holds_alternative<version_up_to_date>(version_info))
128      {
129          state.state = UpdateState::upToDate;
130          state.releasePageUrl = {};
131          state.downloadedInstallerFilename = {};
132          Logger::trace(L"Version is up to date");
133          return;
134      }
135      const auto new_version_info = std::get<new_version_download_info>(version_info);
136      state.releasePageUrl = new_version_info.release_page_uri.ToString().c_str();
137      Logger::trace(L"Discovered new version {}", new_version_info.version.toWstring());
138  
139      const bool already_downloaded = state.state == UpdateState::readyToInstall && state.downloadedInstallerFilename == new_version_info.installer_filename;
140      if (already_downloaded)
141      {
142          Logger::trace(L"New version is already downloaded");
143          return;
144      }
145  
146      // Check toast notification GPOs and settings. (We check only if notifications are allowed. This is the case if we are triggered by the periodic check.)
147      // Disable notification GPO or setting
148      bool disable_notification_setting = get_general_settings().showNewUpdatesToastNotification == false;
149      if (show_notifications && (disable_notification_setting || powertoys_gpo::getDisableNewUpdateToastValue() == powertoys_gpo::gpo_rule_configured_enabled))
150      {
151          Logger::info(L"There is a new update available or ready to install. But the toast notification is disabled by setting or GPO.");
152          show_notifications = false;
153      }
154      // Suspend notification GPO
155      else if (show_notifications && powertoys_gpo::getSuspendNewUpdateToastValue() == powertoys_gpo::gpo_rule_configured_enabled)
156      {
157          Logger::info(L"GPO to suspend new update toast notification is enabled.");
158          if (new_version_info.version.major <= VERSION_MAJOR && new_version_info.version.minor - VERSION_MINOR <= UPDATE_NOTIFICATION_TOAST_SUSPEND_MINOR_VERSION_COUNT)
159          {
160              Logger::info(L"The difference between the installed version and the newer version is within the allowed period. The toast notification is not shown.");
161              show_notifications = false;
162          }
163          else
164          {
165              Logger::info(L"The installed version is older than allowed for suspending the toast notification. The toast notification is shown.");
166          }
167      }
168  
169      if (download_update)
170      {
171          Logger::trace(L"Downloading installer for a new version");
172  
173          // Cleanup old updates before downloading the latest
174          updating::cleanup_updates();
175  
176          if (download_new_version(new_version_info).get())
177          {
178              state.state = UpdateState::readyToInstall;
179              state.downloadedInstallerFilename = new_version_info.installer_filename;
180              Trace::UpdateDownloadCompleted(true, new_version_info.version.toWstring());
181              if (show_notifications)
182              {
183                  ShowNewVersionAvailable(new_version_info);
184              }
185          }
186          else
187          {
188              state.state = UpdateState::errorDownloading;
189              state.downloadedInstallerFilename = {};
190              Trace::UpdateDownloadCompleted(false, new_version_info.version.toWstring());
191              Logger::error("Couldn't download new installer");
192          }
193      }
194      else
195      {
196          Logger::trace(L"New version is ready to download, showing notification");
197          state.state = UpdateState::readyToDownload;
198          state.downloadedInstallerFilename = {};
199          if (show_notifications)
200          {
201              ShowOpenSettingsForUpdate();
202          }
203      }
204  }
205  
206  void PeriodicUpdateWorker()
207  {
208      for (;;)
209      {
210          auto state = UpdateState::read();
211          int64_t sleep_minutes_till_next_update = UPDATE_CHECK_AFTER_FAILED_INTERVAL_MINUTES;
212          if (state.githubUpdateLastCheckedDate.has_value())
213          {
214              int64_t last_checked_minutes_ago = timeutil::diff::in_minutes(timeutil::now(), *state.githubUpdateLastCheckedDate);
215              if (last_checked_minutes_ago < 0)
216              {
217                  last_checked_minutes_ago = UPDATE_CHECK_INTERVAL_MINUTES;
218              }
219              sleep_minutes_till_next_update = max(0, UPDATE_CHECK_INTERVAL_MINUTES - last_checked_minutes_ago);
220          }
221  
222          std::this_thread::sleep_for(std::chrono::minutes{ sleep_minutes_till_next_update });
223  
224          // Auto download setting.
225          bool download_update = !IsMeteredConnection() && get_general_settings().downloadUpdatesAutomatically;
226          if (powertoys_gpo::getDisableAutomaticUpdateDownloadValue() == powertoys_gpo::gpo_rule_configured_enabled)
227          {
228              Logger::info(L"Automatic download of updates is disabled by GPO.");
229              download_update = false;
230          }
231  
232          bool version_info_obtained = false;
233          try
234          {
235              const auto new_version_info = get_github_version_info_async().get();
236              if (new_version_info.has_value())
237              {
238                  version_info_obtained = true;
239                  bool updateAvailable = std::holds_alternative<new_version_download_info>(*new_version_info);
240                  std::wstring fromVersion = get_product_version();
241                  std::wstring toVersion = updateAvailable ? std::get<new_version_download_info>(*new_version_info).version.toWstring() : L"";
242                  Trace::UpdateCheckCompleted(true, updateAvailable, fromVersion, toVersion);
243                  ProcessNewVersionInfo(*new_version_info, state, download_update, true);
244              }
245              else
246              {
247                  Trace::UpdateCheckCompleted(false, false, get_product_version(), L"");
248                  Logger::error(L"Couldn't obtain version info from github: {}", new_version_info.error());
249              }
250          }
251          catch (...)
252          {
253              Logger::error("periodic_update_worker: error while processing version info");
254          }
255  
256          if (version_info_obtained)
257          {
258              UpdateState::store([&](UpdateState& v) {
259                  v = std::move(state);
260              });
261          }
262          else
263          {
264              std::this_thread::sleep_for(std::chrono::minutes{ UPDATE_CHECK_AFTER_FAILED_INTERVAL_MINUTES });
265          }
266      }
267  }
268  
269  void CheckForUpdatesCallback()
270  {
271      Logger::trace(L"Check for updates callback invoked");
272      auto state = UpdateState::read();
273      try
274      {
275          auto new_version_info = get_github_version_info_async().get();
276          if (!new_version_info)
277          {
278              // We couldn't get a new version from github for some reason, log error
279              state.state = UpdateState::networkError;
280              Trace::UpdateCheckCompleted(false, false, get_product_version(), L"");
281              Logger::error(L"Couldn't obtain version info from github: {}", new_version_info.error());
282          }
283          else
284          {
285              // Auto download setting
286              bool download_update = !IsMeteredConnection() && get_general_settings().downloadUpdatesAutomatically;
287              if (powertoys_gpo::getDisableAutomaticUpdateDownloadValue() == powertoys_gpo::gpo_rule_configured_enabled)
288              {
289                  Logger::info(L"Automatic download of updates is disabled by GPO.");
290                  download_update = false;
291              }
292  
293              bool updateAvailable = std::holds_alternative<new_version_download_info>(*new_version_info);
294              std::wstring fromVersion = get_product_version();
295              std::wstring toVersion = updateAvailable ? std::get<new_version_download_info>(*new_version_info).version.toWstring() : L"";
296              Trace::UpdateCheckCompleted(true, updateAvailable, fromVersion, toVersion);
297              ProcessNewVersionInfo(*new_version_info, state, download_update, false);
298          }
299  
300          UpdateState::store([&](UpdateState& v) {
301              v = std::move(state);
302          });
303      }
304      catch (...)
305      {
306          Logger::error("CheckForUpdatesCallback: error while processing version info");
307      }
308  }