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 }