diff --git a/PowerToys.sln b/PowerToys.sln index 46272018cc..d46758945b 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -344,8 +344,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FancyZonesModuleInterface", EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FancyZones", "src\modules\fancyzones\FancyZones\FancyZones.vcxproj", "{FF1D7936-842A-4BBB-8BEA-E9FE796DE700}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToys.Update", "src\Update\PowerToys.Update.vcxproj", "{44CE9AE1-4390-42C5-BACC-0FD6B40AA203}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.WindowsSettings", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.WindowsSettings\Microsoft.PowerToys.Run.Plugin.WindowsSettings.csproj", "{5043CECE-E6A7-4867-9CBE-02D27D83747A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PdfThumbnailProvider", "src\modules\previewpane\PdfThumbnailProvider\PdfThumbnailProvider.csproj", "{11491FD8-F921-48BF-880C-7FEA185B80A1}" @@ -830,6 +828,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewMod EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RunnerV2", "src\RunnerV2\RunnerV2\RunnerV2.csproj", "{20C43796-E14D-47B2-843A-843CAC9C0D28}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Update", "src\Update\Update.csproj", "{9BC7C461-FE76-4F27-B5CB-129F9923967C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -1502,14 +1502,6 @@ Global {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Release|ARM64.Build.0 = Release|ARM64 {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Release|x64.ActiveCfg = Release|x64 {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Release|x64.Build.0 = Release|x64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Debug|ARM64.Build.0 = Debug|ARM64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Debug|x64.ActiveCfg = Debug|x64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Debug|x64.Build.0 = Debug|x64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Release|ARM64.ActiveCfg = Release|ARM64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Release|ARM64.Build.0 = Release|ARM64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Release|x64.ActiveCfg = Release|x64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Release|x64.Build.0 = Release|x64 {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|ARM64.ActiveCfg = Debug|ARM64 {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|ARM64.Build.0 = Debug|ARM64 {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|x64.ActiveCfg = Debug|x64 @@ -3016,6 +3008,14 @@ Global {20C43796-E14D-47B2-843A-843CAC9C0D28}.Release|ARM64.Build.0 = Release|ARM64 {20C43796-E14D-47B2-843A-843CAC9C0D28}.Release|x64.ActiveCfg = Release|x64 {20C43796-E14D-47B2-843A-843CAC9C0D28}.Release|x64.Build.0 = Release|x64 + {9BC7C461-FE76-4F27-B5CB-129F9923967C}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {9BC7C461-FE76-4F27-B5CB-129F9923967C}.Debug|ARM64.Build.0 = Debug|ARM64 + {9BC7C461-FE76-4F27-B5CB-129F9923967C}.Debug|x64.ActiveCfg = Debug|x64 + {9BC7C461-FE76-4F27-B5CB-129F9923967C}.Debug|x64.Build.0 = Debug|x64 + {9BC7C461-FE76-4F27-B5CB-129F9923967C}.Release|ARM64.ActiveCfg = Release|ARM64 + {9BC7C461-FE76-4F27-B5CB-129F9923967C}.Release|ARM64.Build.0 = Release|ARM64 + {9BC7C461-FE76-4F27-B5CB-129F9923967C}.Release|x64.ActiveCfg = Release|x64 + {9BC7C461-FE76-4F27-B5CB-129F9923967C}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/RunnerV2/RunnerV2/Helpers/SettingsHelper.cs b/src/RunnerV2/RunnerV2/Helpers/SettingsHelper.cs index a9d288bc61..61567ca4c2 100644 --- a/src/RunnerV2/RunnerV2/Helpers/SettingsHelper.cs +++ b/src/RunnerV2/RunnerV2/Helpers/SettingsHelper.cs @@ -10,9 +10,11 @@ using System.IO; using System.IO.Pipelines; using System.Linq; using System.Text.Json; +using System.Threading; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using PowerToys.Interop; +using Update; using Windows.Media.Devices; namespace RunnerV2.Helpers @@ -125,6 +127,12 @@ namespace RunnerV2.Helpers ElevationHelper.RestartScheduled = ElevationHelper.RestartScheduledMode.RestartElevatedWithOpenSettings; Runner.Close(); break; + case "restart_mentain_elevation": + // Todo: + break; + case "check_for_updates": + UpdateSettingsHelper.TriggerUpdateCheck(); + break; case "request_update_state_date": // Todo: break; @@ -156,7 +164,15 @@ namespace RunnerV2.Helpers Runner.Close(); break; case "general": - _settingsUtils.SaveSettings(property.Value.ToString(), string.Empty); + try + { + _settingsUtils.SaveSettings(property.Value.ToString(), string.Empty); + } + catch (Exception) + { + // TODO: Log error + } + NativeMethods.PostMessageW(Runner.RunnerHwnd, (uint)NativeMethods.WindowMessages.REFRESH_SETTINGS, 0, 0); foreach (IPowerToysModule module in Runner.ModulesToLoad) diff --git a/src/RunnerV2/RunnerV2/Models/SpecialMode.cs b/src/RunnerV2/RunnerV2/Models/SpecialMode.cs index 178a34e4ca..686f0891bd 100644 --- a/src/RunnerV2/RunnerV2/Models/SpecialMode.cs +++ b/src/RunnerV2/RunnerV2/Models/SpecialMode.cs @@ -9,6 +9,7 @@ namespace RunnerV2.Models None, Win32ToastNotificationCOMServer, ToastNotificationHandler, + UpdateNow, ReportSuccessfulUpdate, } } diff --git a/src/RunnerV2/RunnerV2/Program.cs b/src/RunnerV2/RunnerV2/Program.cs index 1c30588d28..951a766698 100644 --- a/src/RunnerV2/RunnerV2/Program.cs +++ b/src/RunnerV2/RunnerV2/Program.cs @@ -26,6 +26,9 @@ internal sealed class Program { case SpecialMode.None: break; + case SpecialMode.UpdateNow: + UpdateNow(); + return; default: throw new NotImplementedException("Special modes are not implemented yet."); } @@ -103,7 +106,28 @@ internal sealed class Program private static SpecialMode ShouldRunInSpecialMode(string[] args) { - // TODO + if (args.Length > 0 && args[0].StartsWith("powertoys://", StringComparison.InvariantCultureIgnoreCase)) + { + Uri uri = new(args[0]); + string host = uri.Host.ToLowerInvariant(); + return host switch + { + "update_now" => SpecialMode.UpdateNow, + _ => SpecialMode.None, + }; + } + return SpecialMode.None; } + + private static void UpdateNow() + { + Process.Start(new ProcessStartInfo() + { + UseShellExecute = false, + CreateNoWindow = true, + FileName = "PowerToys.Update.exe", + Arguments = "-update_now", + }); + } } diff --git a/src/RunnerV2/RunnerV2/Runner.cs b/src/RunnerV2/RunnerV2/Runner.cs index d6a4adff66..251a28169d 100644 --- a/src/RunnerV2/RunnerV2/Runner.cs +++ b/src/RunnerV2/RunnerV2/Runner.cs @@ -13,9 +13,11 @@ using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; using System.Windows.Forms; using ManagedCommon; using RunnerV2.Helpers; +using Update; using static RunnerV2.NativeMethods; namespace RunnerV2 @@ -44,6 +46,8 @@ namespace RunnerV2 { TrayIconManager.StartTrayIcon(); + Task.Run(UpdateUtilities.UninstallPreviousMsixVersions); + foreach (IPowerToysModule module in ModulesToLoad) { ToggleModuleStateBasedOnEnabledProperty(module); @@ -106,29 +110,33 @@ namespace RunnerV2 public static void ToggleModuleStateBasedOnEnabledProperty(IPowerToysModule module) { - if ((module.Enabled && (module.GpoRuleConfigured != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)) || module.GpoRuleConfigured == PowerToys.GPOWrapper.GpoRuleConfigured.Enabled) + try { - try + if ((module.Enabled && (module.GpoRuleConfigured != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)) || module.GpoRuleConfigured == PowerToys.GPOWrapper.GpoRuleConfigured.Enabled) { - module.Enable(); - /* Todo: conflict manager */ - foreach (var hotkey in module.Hotkeys) + // ToArray is called to mitigate mutations while the foreach is executing + foreach (var hotkey in module.Hotkeys.ToArray()) { HotkeyManager.EnableHotkey(hotkey.Key, hotkey.Value); } if (!LoadedModules.Contains(module)) { + module.Enable(); LoadedModules.Add(module); } - } - catch (Exception e) - { - MessageBox.Show($"The module {module.Name} failed to load: \n" + e.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } + return; + } + } + catch (IOException) + { + } + catch (Exception e) + { + MessageBox.Show($"The module {module.Name} failed to load: \n" + e.Message, "Error: " + e.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error); return; } @@ -143,14 +151,15 @@ namespace RunnerV2 LoadedModules.Remove(module); } + catch (IOException) + { + } catch (Exception e) { - MessageBox.Show($"The module {module.Name} failed to unload: \n" + e.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show($"The module {module.Name} failed to unload: \n" + e.Message, "Error: " + e.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error); } } - public static Thread? WindowThread { get; set; } - [STAThread] private static void InitializeTrayWindow() { diff --git a/src/RunnerV2/RunnerV2/RunnerV2.csproj b/src/RunnerV2/RunnerV2/RunnerV2.csproj index f8ea6d2596..6776e6acc1 100644 --- a/src/RunnerV2/RunnerV2/RunnerV2.csproj +++ b/src/RunnerV2/RunnerV2/RunnerV2.csproj @@ -4,7 +4,7 @@ WinExe PowerToys Runner - PowerToys2 + PowerToys ..\..\..\$(Platform)\$(Configuration) enable false @@ -16,5 +16,6 @@ + diff --git a/src/Update/PowerToys.Update.base.rc b/src/Update/PowerToys.Update.base.rc deleted file mode 100644 index 5e4e3aab3c..0000000000 --- a/src/Update/PowerToys.Update.base.rc +++ /dev/null @@ -1,36 +0,0 @@ -#include -#include "resource.h" -#include "../common/version/version.h" - -1 VERSIONINFO -FILEVERSION FILE_VERSION -PRODUCTVERSION PRODUCT_VERSION -FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -#ifdef _DEBUG -FILEFLAGS VS_FF_DEBUG -#else -FILEFLAGS 0x0L -#endif -FILEOS VOS_NT_WINDOWS32 -FILETYPE VFT_DLL -FILESUBTYPE VFT2_UNKNOWN -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset - BEGIN - VALUE "CompanyName", COMPANY_NAME - VALUE "FileDescription", FILE_DESCRIPTION - VALUE "FileVersion", FILE_VERSION_STRING - VALUE "InternalName", INTERNAL_NAME - VALUE "LegalCopyright", COPYRIGHT_NOTE - VALUE "OriginalFilename", ORIGINAL_FILENAME - VALUE "ProductName", PRODUCT_NAME - VALUE "ProductVersion", PRODUCT_VERSION_STRING - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset - END -END diff --git a/src/Update/PowerToys.Update.cpp b/src/Update/PowerToys.Update.cpp deleted file mode 100644 index 1e16598c43..0000000000 --- a/src/Update/PowerToys.Update.cpp +++ /dev/null @@ -1,234 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#define WIN32_LEAN_AND_MEAN -#include "Generated Files/resource.h" - -#include -#include - -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include - -#include - -#include -#include -#include - -#include "../runner/tray_icon.h" -#include "../runner/UpdateUtils.h" - -using namespace cmdArg; - -namespace fs = std::filesystem; - -std::optional CopySelfToTempDir() -{ - std::error_code error; - auto dst_path = fs::temp_directory_path() / "PowerToys.Update.exe"; - fs::copy_file(get_module_filename(), dst_path, fs::copy_options::overwrite_existing, error); - if (error) - { - return std::nullopt; - } - - return std::move(dst_path); -} - -std::optional ObtainInstaller(bool& isUpToDate) -{ - using namespace updating; - - isUpToDate = false; - - auto state = UpdateState::read(); - - const auto new_version_info = get_github_version_info_async().get(); - if (std::holds_alternative(*new_version_info)) - { - isUpToDate = true; - Logger::error("Invoked with -update_now argument, but no update was available"); - return std::nullopt; - } - - if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading) - { - if (!new_version_info) - { - Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error()); - return std::nullopt; - } - - // Cleanup old updates before downloading the latest - updating::cleanup_updates(); - - auto downloaded_installer = download_new_version(std::get(*new_version_info)).get(); - if (!downloaded_installer) - { - Logger::error("Couldn't download new installer"); - } - - return downloaded_installer; - } - else if (state.state == UpdateState::readyToInstall) - { - fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename }; - if (fs::is_regular_file(installer)) - { - return std::move(installer); - } - else - { - Logger::error(L"Couldn't find a downloaded installer {}", installer.native()); - return std::nullopt; - } - } - else if (state.state == UpdateState::upToDate) - { - isUpToDate = true; - return std::nullopt; - } - - Logger::error("Invoked with -update_now argument, but update state was invalid"); - return std::nullopt; -} - -bool InstallNewVersionStage1(fs::path installer) -{ - if (auto copy_in_temp = CopySelfToTempDir()) - { - // Detect if PT was running - const auto pt_main_window = FindWindowW(pt_tray_icon_window_class, nullptr); - - if (pt_main_window != nullptr) - { - SendMessageW(pt_main_window, WM_CLOSE, 0, 0); - } - - std::wstring arguments{ UPDATE_NOW_LAUNCH_STAGE2 }; - arguments += L" \""; - arguments += installer.c_str(); - arguments += L"\""; - SHELLEXECUTEINFOW sei{ sizeof(sei) }; - sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC }; - sei.lpFile = copy_in_temp->c_str(); - sei.nShow = SW_SHOWNORMAL; - - sei.lpParameters = arguments.c_str(); - return ShellExecuteExW(&sei) == TRUE; - } - else - { - return false; - } -} - -bool InstallNewVersionStage2(std::wstring installer_path) -{ - std::transform(begin(installer_path), end(installer_path), begin(installer_path), ::towlower); - - bool success = true; - - if (installer_path.ends_with(L".msi")) - { - success = MsiInstallProductW(installer_path.data(), nullptr) == ERROR_SUCCESS; - } - else - { - // If it's not .msi, then it's a wix bootstrapper - SHELLEXECUTEINFOW sei{ sizeof(sei) }; - sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE }; - sei.lpFile = installer_path.c_str(); - sei.nShow = SW_SHOWNORMAL; - std::wstring parameters = L"/passive /norestart"; - sei.lpParameters = parameters.c_str(); - - success = ShellExecuteExW(&sei) == TRUE; - - // Wait for the install completion - if (success) - { - WaitForSingleObject(sei.hProcess, INFINITE); - DWORD exitCode = 0; - GetExitCodeProcess(sei.hProcess, &exitCode); - success = exitCode == 0; - CloseHandle(sei.hProcess); - } - } - - if (!success) - { - return false; - } - - UpdateState::store([&](UpdateState& state) { - state = {}; - state.githubUpdateLastCheckedDate.emplace(timeutil::now()); - state.state = UpdateState::upToDate; - }); - - return true; -} - -int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) -{ - int nArgs = 0; - LPWSTR* args = CommandLineToArgvW(GetCommandLineW(), &nArgs); - if (!args || nArgs < 2) - { - return 1; - } - - std::wstring_view action{ args[1] }; - - std::filesystem::path logFilePath(PTSettingsHelper::get_root_save_folder_location()); - logFilePath.append(LogSettings::updateLogPath); - Logger::init(LogSettings::updateLoggerName, logFilePath.wstring(), PTSettingsHelper::get_log_settings_file_location()); - - if (action == UPDATE_NOW_LAUNCH_STAGE1) - { - bool isUpToDate = false; - auto installerPath = ObtainInstaller(isUpToDate); - bool failed = !installerPath.has_value(); - failed = failed || !InstallNewVersionStage1(std::move(*installerPath)); - if (failed) - { - UpdateState::store([&](UpdateState& state) { - state = {}; - state.githubUpdateLastCheckedDate.emplace(timeutil::now()); - state.state = isUpToDate ? UpdateState::upToDate : UpdateState::errorDownloading; - }); - } - return failed; - } - else if (action == UPDATE_NOW_LAUNCH_STAGE2) - { - using namespace std::string_view_literals; - const bool failed = !InstallNewVersionStage2(args[2]); - if (failed) - { - UpdateState::store([&](UpdateState& state) { - state = {}; - state.githubUpdateLastCheckedDate.emplace(timeutil::now()); - state.state = UpdateState::errorDownloading; - }); - } - return failed; - } - - return 0; -} diff --git a/src/Update/PowerToys.Update.vcxproj b/src/Update/PowerToys.Update.vcxproj deleted file mode 100644 index 172a7027a6..0000000000 --- a/src/Update/PowerToys.Update.vcxproj +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - 16.0 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203} - Update - PowerToys.Update - - - - v143 - - - - Application - - - - - - - - - - - - - Level4 - NotUsing - ../;%(AdditionalIncludeDirectories) - - - WindowsApp.lib;Msi.lib;Shlwapi.lib;%(AdditionalDependencies) - - - - - - - - {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} - - - {1d5be09d-78c0-4fd7-af00-ae7c1af7c525} - - - {6955446d-23f7-4023-9bb3-8657f904af99} - - - {17da04df-e393-4397-9cf0-84dabe11032e} - - - - - - - - - - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - \ No newline at end of file diff --git a/src/Update/Program.cs b/src/Update/Program.cs new file mode 100644 index 0000000000..deeb281c86 --- /dev/null +++ b/src/Update/Program.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Threading.Tasks; +using Update; + +[SupportedOSPlatform("windows")] +internal sealed partial class Program +{ + private static readonly string _installerPath = Path.Combine(Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Microsoft", + "PowerToys", + "Updates")); + + private static async Task Main(string[] args) + { + if (args.Length < 1) + { + Environment.Exit(1); + return; + } + + string action = args[0]; + + switch (action) + { + case UpdateStage.UPDATENOWLAUNCHSTAGE1: + await PerformUpdateNowStage1(); + break; + case UpdateStage.UPDATENOWLAUNCHSTAGE2: + if (args.Length < 2) + { + Environment.Exit(1); + } + + await PerformUpdateNowStage2(args[1]); + break; + default: + break; + } + } + + private static async Task PerformUpdateNowStage2(string installerPath) + { + Process installerProcess = new() + { + StartInfo = new ProcessStartInfo + { + FileName = installerPath, + Arguments = "/passive /norestart", + UseShellExecute = true, + }, + }; + + installerProcess.Start(); + await installerProcess.WaitForExitAsync(); + + if (installerProcess.ExitCode == 0) + { + UpdateSettingsHelper.ProcessNoUpdateAvailable(); + } + else + { + UpdateSettingsHelper.SetUpdateState(UpdatingSettings.UpdatingState.ErrorDownloading); + } + } + + private static async Task PerformUpdateNowStage1() + { + UpdateSettingsHelper.TriggerUpdateCheck(); + UpdateSettingsHelper.UpdateInfo updateInfo = await UpdateSettingsHelper.GetUpdateAvailableInfo(); + + if (updateInfo is not UpdateSettingsHelper.UpdateInfo.UpdateAvailable ua) + { + // No update found + Environment.Exit(1); + return; + } + + // Copy itsself to the temp folder + File.Copy("PowerToys.Update.exe", Path.Combine(Path.GetTempPath(), "PowerToys.Update.exe"), true); + + string? installerFilePath = null; + + switch (UpdateSettingsHelper.GetUpdateState()) + { + case UpdatingSettings.UpdatingState.ReadyToDownload: + case UpdatingSettings.UpdatingState.ErrorDownloading: + CleanupUpdates(); + installerFilePath = await DownloadFile(ua.InstallerDownloadUrl.ToString(), ua.InstallerFilename); + break; + case UpdatingSettings.UpdatingState.ReadyToInstall: + installerFilePath = Path.Combine(_installerPath, ua.InstallerFilename); + if (!File.Exists(installerFilePath)) + { + // Installer not found + Environment.Exit(1); + return; + } + + break; + case UpdatingSettings.UpdatingState.UpToDate: + Environment.Exit(0); + return; + } + + if (installerFilePath == null) + { + UpdateSettingsHelper.SetUpdateState(UpdatingSettings.UpdatingState.ErrorDownloading); + Environment.Exit(1); + return; + } + + IntPtr runnerHwnd = FindWindowW("pt_tray_icon_window_class"); + + if (runnerHwnd != IntPtr.Zero) + { + SendMessageW(runnerHwnd, 0x0010, IntPtr.Zero, IntPtr.Zero); // Send WM_CLOSE + } + + string arguments = $"{UpdateStage.UPDATENOWLAUNCHSTAGE2} \"{installerFilePath}\""; + + Process.Start(new ProcessStartInfo + { + FileName = Path.Combine(Path.GetTempPath(), "PowerToys.Update.exe"), + Arguments = arguments, + UseShellExecute = true, + CreateNoWindow = true, + WorkingDirectory = Environment.CurrentDirectory, + }); + } + + private static async Task DownloadFile(string downloadUri, string downloadFileName) + { + HttpClient httpClient = new(); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("PowerToys Runner"); // GitHub API requires a user-agent + + // 3 Attempts to download the file + for (int i = 0; i < 3; i++) + { + try + { + using FileStream fileStream = new(Path.Combine(_installerPath, downloadFileName), FileMode.Create, FileAccess.Write, FileShare.None); + await (await httpClient.GetStreamAsync(downloadUri)).CopyToAsync(fileStream); + return fileStream.Name; + } + catch + { + } + } + + return null; + } + + private static void CleanupUpdates() + { + if (!Path.Exists(_installerPath)) + { + return; + } + + foreach (string file in Directory.GetFiles(_installerPath).Where(f => f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))) + { + File.Delete(file); + } + } + + [LibraryImport("user32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + private static partial IntPtr FindWindowW(string lpClassName); + + [LibraryImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SendMessageW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); +} diff --git a/src/Update/Resources.resx b/src/Update/Resources.resx deleted file mode 100644 index a141dacd3d..0000000000 --- a/src/Update/Resources.resx +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Couldn't download .NET Core Desktop Runtime 3.1, please install it manually. - - - PowerToys installation error - - - An update to PowerToys is available. - - - Update now - - - An update to PowerToys is available. Visit our GitHub page to update. - - - More info... - - - PowerToys Update - - diff --git a/src/Update/Update.csproj b/src/Update/Update.csproj new file mode 100644 index 0000000000..bf3ee21584 --- /dev/null +++ b/src/Update/Update.csproj @@ -0,0 +1,22 @@ + + + + + WinExe + PowerToys Runner + PowerToys.Update + ..\..\$(Platform)\$(Configuration) + enable + false + false + true + true + + + + <_IsPublishing Condition="'$(_IsPublishing)'==''">false + + + + + diff --git a/src/Update/UpdateSettingsHelper.cs b/src/Update/UpdateSettingsHelper.cs new file mode 100644 index 0000000000..8d1899cd5c --- /dev/null +++ b/src/Update/UpdateSettingsHelper.cs @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.IO; +using System.Net.Http; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32; + +namespace Update +{ + [SupportedOSPlatform("windows")] + public static class UpdateSettingsHelper + { + private static Thread? _updateThread; + + private const string INSTALLERFILENAME = "powertoyssetup"; + private const string USERINSTALLERFILENAME = "powertoysusersetup"; + + public static void TriggerUpdateCheck() + { + if (_updateThread is not null && _updateThread.IsAlive) + { + return; + } + + _updateThread = new Thread(async () => + { + UpdateInfo updateInfo = await GetUpdateAvailableInfo(); + switch (updateInfo) + { + case UpdateInfo.UpdateCheckFailed ucf: + ProcessUpdateCheckFailed(ucf); + break; + case UpdateInfo.UpdateAvailable ua: + ProcessUpdateAvailable(ua); + break; + case UpdateInfo.NoUpdateAvailable: + ProcessNoUpdateAvailable(); + break; + } + }); + + _updateThread.Start(); + } + + internal record UpdateInfo + { + private UpdateInfo() + { + } + + public sealed record NoUpdateAvailable : UpdateInfo; + + public sealed record UpdateAvailable(Uri ReleasePageUri, Version AvailableVersion, Uri InstallerDownloadUrl, string InstallerFilename) : UpdateInfo; + + public sealed record UpdateCheckFailed(Exception Exception) : UpdateInfo; + } + + internal static async Task GetUpdateAvailableInfo() + { + Version? currentVersion = Assembly.GetExecutingAssembly().GetName().Version; + + if (currentVersion is null) + { + // Todo: Log + return new UpdateInfo.NoUpdateAvailable(); + } + + if (currentVersion is { Major: 0, Minor: 0 }) + { + // Pre-release or local build, skip update check + return new UpdateInfo.NoUpdateAvailable(); + } + + try + { + HttpClient httpClient = new(); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("PowerToys Runner"); // GitHub API requires a user-agent + Stream body = await httpClient.GetStreamAsync("https://api.github.com/repos/microsoft/PowerToys/releases/latest").ConfigureAwait(false); + JsonElement releaseObject = (await JsonDocument.ParseAsync(body)).RootElement; + Version latestVersion = new(releaseObject.GetProperty("tag_name").GetString()?.TrimStart('V', 'v') ?? throw new FormatException("The \"tag_name\" field could not be found")); + string architectureString = RuntimeInformation.OSArchitecture switch + { + Architecture.X64 => "x64", + Architecture.Arm64 => "arm64", + _ => throw new InvalidDataException("Unknown architecture"), + }; + + if (latestVersion > currentVersion) + { + Uri releasePageUri = new(releaseObject.GetProperty("html_url").GetString() ?? throw new FormatException("The \"html_url\" field could not be found")); + + string requiredFilename = GetInstallScope() == InstallScope.PerMachine ? INSTALLERFILENAME : USERINSTALLERFILENAME; + + Uri? installerDownloadUrl = null; + string? installerFilename = null; + + foreach (JsonElement asset in releaseObject.GetProperty("assets").EnumerateArray()) + { + string? name = asset.GetProperty("name").GetString(); + string? browserDownloadUrl = asset.GetProperty("browser_download_url").GetString(); + + if (name is null + || browserDownloadUrl is null + || !name.Contains(requiredFilename, StringComparison.InvariantCultureIgnoreCase) + || !name.Contains(".exe", StringComparison.InvariantCultureIgnoreCase) + || !name.Contains(architectureString, StringComparison.InvariantCultureIgnoreCase)) + { + continue; + } + + installerDownloadUrl = new Uri(browserDownloadUrl); + installerFilename = name; + break; + } + + return installerDownloadUrl is null || installerFilename is null + ? new UpdateInfo.UpdateCheckFailed(new InvalidDataException("No installer found in GitHub release")) + : new UpdateInfo.UpdateAvailable(releasePageUri, latestVersion, installerDownloadUrl, installerFilename); + } + + return new UpdateInfo.NoUpdateAvailable(); + } + catch (Exception e) + { + return new UpdateInfo.UpdateCheckFailed(e); + } + } + + private enum InstallScope + { + PerMachine, + PerUser, + } + + [SupportedOSPlatform("windows")] + private static InstallScope GetInstallScope() + { + if (Registry.LocalMachine.OpenSubKey(@"Software\Classes\powertoys\", false) is not RegistryKey machineKey) + { + if (Registry.CurrentUser.OpenSubKey(@"Software\Classes\powertoys\", false) is not RegistryKey userKey) + { + // Both keys are missing + return InstallScope.PerMachine; + } + + if (userKey.GetValue("InstallScope") is not string installScope) + { + userKey.Close(); + return InstallScope.PerMachine; + } + + userKey.Close(); + + return installScope.Contains("perUser") ? InstallScope.PerUser : InstallScope.PerMachine; + } + + machineKey.Close(); + + return InstallScope.PerMachine; + } + + private static readonly string _settingsPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Microsoft", + "PowerToys"); + + private static readonly string _updatingSettingsFile = Path.Combine(_settingsPath, "UpdateState.json"); + + private static void ProcessUpdateAvailable(UpdateInfo.UpdateAvailable updateAvailable) + { + UpdatingSettings updatingSettings = UpdatingSettings.LoadSettings(); + Console.WriteLine($"Update available: {updateAvailable.AvailableVersion}"); + + updatingSettings.State = UpdatingSettings.UpdatingState.ReadyToDownload; + updatingSettings.ReleasePageLink = updateAvailable.ReleasePageUri.ToString(); + updatingSettings.DownloadedInstallerFilename = updateAvailable.InstallerFilename; + updatingSettings.ReleasePageLink = updateAvailable.ReleasePageUri.ToString(); + updatingSettings.LastCheckedDate = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture); + + File.WriteAllText(_updatingSettingsFile, updatingSettings.ToJsonString()); + } + + internal static void ProcessNoUpdateAvailable() + { + UpdatingSettings updatingSettings = UpdatingSettings.LoadSettings(); + + updatingSettings.State = UpdatingSettings.UpdatingState.UpToDate; + updatingSettings.ReleasePageLink = string.Empty; + updatingSettings.DownloadedInstallerFilename = string.Empty; + updatingSettings.ReleasePageLink = string.Empty; + updatingSettings.LastCheckedDate = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture); + File.WriteAllText(_updatingSettingsFile, updatingSettings.ToJsonString()); + } + + private static void ProcessUpdateCheckFailed(UpdateInfo.UpdateCheckFailed updateCheckFailed) + { + // Todo: Log failed attempt + UpdatingSettings updatingSettings = UpdatingSettings.LoadSettings(); + + updatingSettings.State = UpdatingSettings.UpdatingState.NetworkError; + updatingSettings.ReleasePageLink = string.Empty; + updatingSettings.DownloadedInstallerFilename = string.Empty; + updatingSettings.ReleasePageLink = string.Empty; + updatingSettings.LastCheckedDate = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture); + File.WriteAllText(_updatingSettingsFile, updatingSettings.ToJsonString()); + } + + internal static void SetUpdateState(UpdatingSettings.UpdatingState state) + { + UpdatingSettings updatingSettings = UpdatingSettings.LoadSettings(); + + updatingSettings.State = state; + File.WriteAllText(_updatingSettingsFile, updatingSettings.ToJsonString()); + } + + internal static UpdatingSettings.UpdatingState GetUpdateState() => UpdatingSettings.LoadSettings().State; + } +} diff --git a/src/Update/UpdateStage.cs b/src/Update/UpdateStage.cs new file mode 100644 index 0000000000..f437c779c1 --- /dev/null +++ b/src/Update/UpdateStage.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Update +{ + internal static class UpdateStage + { + internal const string UPDATENOWLAUNCHSTAGE1 = "-update_now"; + internal const string UPDATENOWLAUNCHSTAGE2 = "-update_now_stage_2"; + } +} diff --git a/src/Update/UpdateUtilities.cs b/src/Update/UpdateUtilities.cs new file mode 100644 index 0000000000..f92c499f80 --- /dev/null +++ b/src/Update/UpdateUtilities.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Update +{ + public static class UpdateUtilities + { + public static async void UninstallPreviousMsixVersions() + { + try + { + Windows.Management.Deployment.PackageManager packageManager = new(); + var packages = packageManager.FindPackagesForUser(string.Empty, "Microsoft.PowerToys", "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"); + + Version? currentVersion = Assembly.GetExecutingAssembly().GetName().Version; + + if (currentVersion == null) + { + return; + } + + foreach (var package in packages) + { + Version msixVersion = new Version(package.Id.Version.Major, package.Id.Version.Minor, package.Id.Version.Revision); + if (msixVersion < currentVersion) + { + await packageManager.RemovePackageAsync(package.Id.FullName); + } + } + } + catch + { + } + } + } +} diff --git a/src/Update/UpdatingSettings.cs b/src/Update/UpdatingSettings.cs new file mode 100644 index 0000000000..a001dff16e --- /dev/null +++ b/src/Update/UpdatingSettings.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Update +{ + public sealed class UpdatingSettings + { + public enum UpdatingState + { + UpToDate = 0, + ErrorDownloading, + ReadyToDownload, + ReadyToInstall, + NetworkError, + } + + // Gets or sets a value of the updating state + [JsonPropertyName("state")] + public UpdatingState State { get; set; } + + // Gets or sets a value of the release page url + [JsonPropertyName("releasePageUrl")] + public string ReleasePageLink { get; set; } = string.Empty; + + // Gets or sets a value of the github last checked date + [JsonPropertyName("githubUpdateLastCheckedDate")] + public string LastCheckedDate { get; set; } = string.Empty; + + // Gets or sets a value of the updating state + [JsonPropertyName("downloadedInstallerFilename")] + public string DownloadedInstallerFilename { get; set; } = string.Empty; + + // Non-localizable strings: Files + public const string SettingsFilePath = "\\Microsoft\\PowerToys\\"; + public const string SettingsFile = "UpdateState.json"; + + public string NewVersion + { + get + { + if (ReleasePageLink == null) + { + return string.Empty; + } + + try + { + string version = ReleasePageLink.Substring(ReleasePageLink.LastIndexOf('/') + 1); + return version.Trim(); + } + catch (Exception) + { + } + + return string.Empty; + } + } + + public string LastCheckedDateLocalized + { + get + { + try + { + if (LastCheckedDate == null) + { + return string.Empty; + } + + long seconds = long.Parse(LastCheckedDate, CultureInfo.CurrentCulture); + var date = DateTimeOffset.FromUnixTimeSeconds(seconds).UtcDateTime; + return date.ToLocalTime().ToString(CultureInfo.CurrentCulture); + } + catch (Exception) + { + } + + return string.Empty; + } + } + + public UpdatingSettings() + { + State = UpdatingState.UpToDate; + } + + public static UpdatingSettings LoadSettings() + { + var localAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var file = localAppDataDir + SettingsFilePath + SettingsFile; + + if (File.Exists(file)) + { + try + { + FileStream inputStream = File.Open(file, FileMode.Open); + StreamReader reader = new(inputStream); + string data = reader.ReadToEnd(); + inputStream.Close(); + reader.Dispose(); + + return JsonSerializer.Deserialize(data, UpdatingsSettingsSourceGenerationContext.Default.UpdatingSettings)!; + } + catch (Exception) + { + } + } + + return new UpdatingSettings(); + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this, UpdatingsSettingsSourceGenerationContext.Default.UpdatingSettings); + } + } +} diff --git a/src/Update/UpdatingsSettingsSourceGenerationContext.cs b/src/Update/UpdatingsSettingsSourceGenerationContext.cs new file mode 100644 index 0000000000..aceb5938ab --- /dev/null +++ b/src/Update/UpdatingsSettingsSourceGenerationContext.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; + +namespace Update +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(UpdatingSettings))] + + internal sealed partial class UpdatingsSettingsSourceGenerationContext : JsonSerializerContext + { + } +} diff --git a/src/Update/packages.config b/src/Update/packages.config deleted file mode 100644 index ff4b059648..0000000000 --- a/src/Update/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/Update/resource.base.h b/src/Update/resource.base.h deleted file mode 100644 index eaef556513..0000000000 --- a/src/Update/resource.base.h +++ /dev/null @@ -1,11 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by PowerToys.Update.rc - -////////////////////////////// -// Non-localizable - -#define FILE_DESCRIPTION "PowerToys Update" -#define INTERNAL_NAME "PowerToys.Update" -#define ORIGINAL_FILENAME "PowerToys.Update.exe" -