From 554f972bd1e44f4d0559ea05e37302fe7dd5ad7d Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Thu, 14 Mar 2024 11:21:14 -0400 Subject: [PATCH 001/121] Have IThemeSelectorService notify for ActualThemeChanged (#2395) --- common/Services/IThemeSelectorService.cs | 3 +++ src/ViewModels/ShellViewModel.cs | 20 ++++++++++--------- src/Views/ShellPage.xaml.cs | 7 +++---- .../Services/AdaptiveCardRenderingService.cs | 6 +++++- .../Services/IAdaptiveCardRenderingService.cs | 3 ++- .../Views/DashboardView.xaml | 11 ++++++++++ .../Views/DashboardView.xaml.cs | 19 +++++++++--------- .../ViewModels/RepoConfigViewModel.cs | 11 ++-------- .../Views/RepoConfigView.xaml.cs | 9 --------- 9 files changed, 47 insertions(+), 42 deletions(-) diff --git a/common/Services/IThemeSelectorService.cs b/common/Services/IThemeSelectorService.cs index b55fcb2fa2..8d02e5e1b0 100644 --- a/common/Services/IThemeSelectorService.cs +++ b/common/Services/IThemeSelectorService.cs @@ -9,6 +9,9 @@ namespace DevHome.Contracts.Services; public interface IThemeSelectorService { + /// + /// Occurs when the theme has changed, either due to user selection or the system theme changing. + /// public event EventHandler ThemeChanged; ElementTheme Theme { get; } diff --git a/src/ViewModels/ShellViewModel.cs b/src/ViewModels/ShellViewModel.cs index 96fcdd95f3..1680ee4fd6 100644 --- a/src/ViewModels/ShellViewModel.cs +++ b/src/ViewModels/ShellViewModel.cs @@ -14,21 +14,16 @@ public partial class ShellViewModel : ObservableObject { private readonly ILocalSettingsService _localSettingsService; private readonly IAppInfoService _appInfoService; + private readonly IThemeSelectorService _themeSelectorService; [ObservableProperty] private string? _announcementText; public string Title => _appInfoService.GetAppNameLocalized(); - public INavigationService NavigationService - { - get; - } + public INavigationService NavigationService { get; } - public INavigationViewService NavigationViewService - { - get; - } + public INavigationViewService NavigationViewService { get; } [ObservableProperty] private object? _selected; @@ -41,13 +36,15 @@ public ShellViewModel( INavigationViewService navigationViewService, ILocalSettingsService localSettingsService, IScreenReaderService screenReaderService, - IAppInfoService appInfoService) + IAppInfoService appInfoService, + IThemeSelectorService themeSelectorService) { NavigationService = navigationService; NavigationService.Navigated += OnNavigated; NavigationViewService = navigationViewService; _localSettingsService = localSettingsService; _appInfoService = appInfoService; + _themeSelectorService = themeSelectorService; screenReaderService.AnnouncementTextChanged += OnAnnouncementTextChanged; } @@ -98,4 +95,9 @@ private void OnAnnouncementTextChanged(object? sender, string text) // Set new announcement title AnnouncementText = text; } + + internal void NotifyActualThemeChanged() + { + _themeSelectorService.SetRequestedTheme(); + } } diff --git a/src/Views/ShellPage.xaml.cs b/src/Views/ShellPage.xaml.cs index a51b0d3e17..e8d7e875fe 100644 --- a/src/Views/ShellPage.xaml.cs +++ b/src/Views/ShellPage.xaml.cs @@ -17,10 +17,7 @@ namespace DevHome.Views; public sealed partial class ShellPage : Page { - public ShellViewModel ViewModel - { - get; - } + public ShellViewModel ViewModel { get; } public ShellPage(ShellViewModel viewModel) { @@ -59,6 +56,8 @@ private void OnActualThemeChanged(FrameworkElement sender, object args) // Update the title bar if the system theme changes. TitleBarHelper.UpdateTitleBar(App.MainWindow, ActualTheme); AppTitleBar.Repaint(); + + ViewModel.NotifyActualThemeChanged(); } private void MainWindow_Activated(object sender, WindowActivatedEventArgs args) diff --git a/tools/Dashboard/DevHome.Dashboard/Services/AdaptiveCardRenderingService.cs b/tools/Dashboard/DevHome.Dashboard/Services/AdaptiveCardRenderingService.cs index 3a68c1b8a9..6535f7356c 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/AdaptiveCardRenderingService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/AdaptiveCardRenderingService.cs @@ -16,6 +16,8 @@ namespace DevHome.Dashboard.Services; public class AdaptiveCardRenderingService : IAdaptiveCardRenderingService, IDisposable { + public event EventHandler RendererUpdated = (_, _) => { }; + private readonly WindowEx _windowEx; private readonly IThemeSelectorService _themeSelectorService; @@ -94,7 +96,7 @@ private async Task ConfigureWidgetRenderer() }; } - public async Task UpdateHostConfig() + private async Task UpdateHostConfig() { if (_renderer != null) { @@ -127,6 +129,8 @@ public async Task UpdateHostConfig() Log.Logger()?.ReportError("DashboardView", $"HostConfig contents are {hostConfigContents}"); } }); + + RendererUpdated(this, null); } } diff --git a/tools/Dashboard/DevHome.Dashboard/Services/IAdaptiveCardRenderingService.cs b/tools/Dashboard/DevHome.Dashboard/Services/IAdaptiveCardRenderingService.cs index 07603bd2ea..97e244e446 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/IAdaptiveCardRenderingService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/IAdaptiveCardRenderingService.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Threading.Tasks; using AdaptiveCards.Rendering.WinUI3; @@ -10,5 +11,5 @@ public interface IAdaptiveCardRenderingService { public Task GetRenderer(); - public Task UpdateHostConfig(); + public event EventHandler RendererUpdated; } diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml index 3b3bd3d829..0c02bf3f56 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml @@ -14,6 +14,8 @@ xmlns:controls="using:DevHome.Dashboard.Controls" xmlns:behaviors="using:DevHome.Common.Behaviors" xmlns:converters="using:CommunityToolkit.WinUI.Converters" + xmlns:i="using:Microsoft.Xaml.Interactivity" + xmlns:ic="using:Microsoft.Xaml.Interactions.Core" behaviors:NavigationViewHeaderBehavior.HeaderMode="Never" mc:Ignorable="d"> @@ -33,6 +35,15 @@ + + + + + + + + + diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs index f987179c8c..1317cb488b 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs @@ -66,10 +66,6 @@ public DashboardView() _dispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); _localSettingsService = Application.Current.GetService(); - ActualThemeChanged += OnActualThemeChanged; - - Loaded += OnLoaded; - #if DEBUG Loaded += AddResetButton; #endif @@ -102,11 +98,8 @@ private async Task SubscribeToWidgetCatalogEventsAsync() return true; } - private async void OnActualThemeChanged(FrameworkElement sender, object args) + private async void HandleRendererUpdated(object sender, object args) { - // A different host config is used to render widgets (adaptive cards) in light and dark themes. - await Application.Current.GetService().UpdateHostConfig(); - // Re-render the widgets with the new theme and renderer. foreach (var widget in PinnedWidgets) { @@ -114,11 +107,19 @@ private async void OnActualThemeChanged(FrameworkElement sender, object args) } } - private async void OnLoaded(object sender, RoutedEventArgs e) + [RelayCommand] + private async Task OnLoadedAsync() { + Application.Current.GetService().RendererUpdated += HandleRendererUpdated; await InitializeDashboard(); } + [RelayCommand] + private void OnUnloaded() + { + Application.Current.GetService().RendererUpdated -= HandleRendererUpdated; + } + private async Task InitializeDashboard() { LoadingWidgetsProgressRing.Visibility = Visibility.Visible; diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/RepoConfigViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/RepoConfigViewModel.cs index 7567ba375d..e0b9189210 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/RepoConfigViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/RepoConfigViewModel.cs @@ -118,19 +118,12 @@ public RepoConfigViewModel( : stringResource.GetLocalized(StringResourceKey.SetupShellRepoConfigTargetMachine); } - private void OnThemeChanged(object sender, ElementTheme e) + private void OnThemeChanged(object sender, ElementTheme newRequestedTheme) { - var themeToSwitchTo = e; - - if (themeToSwitchTo == ElementTheme.Default) - { - themeToSwitchTo = _themeSelectorService.GetActualTheme(); - } - // Because the logos aren't glyphs DevHome has to change the logos manually to match the theme. foreach (var cloneInformation in RepoReviewItems) { - cloneInformation.SetIcon(themeToSwitchTo); + cloneInformation.SetIcon(_themeSelectorService.GetActualTheme()); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml.cs index bbcce1ce31..c0a90a74a3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml.cs @@ -39,15 +39,6 @@ public RepoConfigView() public void OnActualThemeChanged(FrameworkElement sender, object args) { - if (ViewModel != null) - { - // Because the logos aren't glyphs DevHome has to change the logos manually to match the theme. - foreach (var cloneInformation in ViewModel.RepoReviewItems) - { - cloneInformation.SetIcon(sender.ActualTheme); - } - } - if (_addRepoDialog != null) { _addRepoDialog.RequestedTheme = sender.ActualTheme; From 4135b62b4f1151657847bb074d421ac66ab7d7d5 Mon Sep 17 00:00:00 2001 From: Jeff Whiteside Date: Thu, 14 Mar 2024 09:16:08 -0700 Subject: [PATCH 002/121] =?UTF-8?q?Experimental=20Feature:=20Quiet=20Backg?= =?UTF-8?q?round=20Processes=20UI=20and=20WinRT=20server=20=E2=80=A6=20(#2?= =?UTF-8?q?322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Experimental Feature: Quiet Background Processes UI and WinRT server (/w Self-Elevation) Adding the "Enable 'Quiet Background Processes'" experimental feature to Dev Home. It's a toggle that is enabled similarly to Focus Assist mode, that expires after a period of time. When you enable it/toggle it on via the UI, Dev Home will call into the OS to enable quieting down background services (e.g. Search Indexer). * Works on supported OSes * Requires a split-token user (for UAC prompt) or Over-The-Shoulder credentials to consume the feature --- .github/workflows/DevHome-CI.yml | 1 + DevHome.sln | 97 +++++++++ Directory.CppBuild.props | 8 + common/DevHome.Common.csproj | 2 +- .../Strings/en-us/Resources.resw | 8 + src/DevHome.csproj | 28 +++ src/NavConfig.jsonc | 29 +++ src/Package.appxmanifest | 18 +- test/DevHome.Test.csproj | 2 +- .../DevHome.Dashboard.UnitTest.csproj | 2 +- .../src/DevHome.Experiments.csproj | 3 +- .../src/Strings/en-us/Resources.resw | 136 ++++++------ tools/QuietBackgroundProcesses/.gitattributes | 3 + ...me.QuietBackgroundProcesses.Common.vcxproj | 195 +++++++++++++++++ .../DevHome.QuietBackgroundProcesses.idl | 25 +++ .../PropertySheet.props | 16 ++ .../Utility.h | 140 ++++++++++++ .../empty.cpp | 5 + ...Processes.ElevatedServer.Projection.csproj | 36 ++++ ...BackgroundProcesses.ElevatedServer.vcxproj | 173 +++++++++++++++ .../PropertySheet.props | 16 ++ .../QuietBackgroundProcessesSession.cpp | 136 ++++++++++++ .../QuietState.cpp | 27 +++ .../QuietState.h | 16 ++ .../TimedQuietSession.h | 143 +++++++++++++ .../Timer.h | 114 ++++++++++ .../main.cpp | 109 ++++++++++ .../packages.config | 5 + .../pch.cpp | 4 + .../pch.h | 10 + ...me.QuietBackgroundProcesses.Server.vcxproj | 175 +++++++++++++++ .../PropertySheet.props | 16 ++ ...QuietBackgroundProcessesSessionManager.cpp | 89 ++++++++ .../main.cpp | 80 +++++++ .../packages.config | 5 + .../pch.cpp | 4 + .../pch.h | 10 + ...DevHome.QuietBackgroundProcesses.UI.csproj | 22 ++ .../Strings/en-us/Resources.resw | 111 ++++++++++ .../QuietBackgroundProcessesViewModel.cs | 200 ++++++++++++++++++ .../Views/QuietBackgroundProcessesPage.xaml | 44 ++++ .../QuietBackgroundProcessesPage.xaml.cs | 26 +++ 42 files changed, 2216 insertions(+), 73 deletions(-) create mode 100644 Directory.CppBuild.props create mode 100644 tools/QuietBackgroundProcesses/.gitattributes create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.Common.vcxproj create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.idl create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/PropertySheet.props create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/Utility.h create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/empty.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/PropertySheet.props create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietBackgroundProcessesSession.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietState.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietState.h create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/TimedQuietSession.h create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Timer.h create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/main.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/packages.config create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/pch.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/pch.h create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/DevHome.QuietBackgroundProcesses.Server.vcxproj create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/PropertySheet.props create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/QuietBackgroundProcessesSessionManager.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/main.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/packages.config create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/pch.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/pch.h create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/DevHome.QuietBackgroundProcesses.UI.csproj create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesPage.xaml create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesPage.xaml.cs diff --git a/.github/workflows/DevHome-CI.yml b/.github/workflows/DevHome-CI.yml index 8c6df009b4..e808d2a70d 100644 --- a/.github/workflows/DevHome-CI.yml +++ b/.github/workflows/DevHome-CI.yml @@ -60,6 +60,7 @@ jobs: run: | cmd /c "$env:VSDevCmd" "&" msbuild -t:restore /m /p:Configuration=Release,Platform=${{ matrix.platform }},RestorePackagesConfig=true extensionsdk\\DevHomeSDK.sln cmd /c "$env:VSDevCmd" "&" msbuild -t:restore /m /p:Configuration=Release,Platform=${{ matrix.platform }} DevHome.sln + cmd /c ".\.nuget\nuget.exe restore" - name: Build_SDK run: cmd /c "$env:VSDevCmd" "&" msbuild /p:Configuration=Release,Platform=${{ matrix.platform }} extensionsdk\\DevHomeSDK.sln diff --git a/DevHome.sln b/DevHome.sln index 068148209b..87ee93969b 100644 --- a/DevHome.sln +++ b/DevHome.sln @@ -72,6 +72,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExtensionLibrary", "Extensi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.Experiments", "tools\Experiments\src\DevHome.Experiments.csproj", "{2F9AD5AF-EF3B-496A-8566-9E9539E3DF43}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "QuietBackgroundProcesses", "QuietBackgroundProcesses", "{D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DevHome.QuietBackgroundProcesses.ElevatedServer", "tools\QuietBackgroundProcesses\DevHome.QuietBackgroundProcesses.ElevatedServer\DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj", "{75945141-03AC-4C40-A586-16D463A0AC1B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.QuietBackgroundProcesses.ElevatedServer.Projection", "tools\QuietBackgroundProcesses\DevHome.QuietBackgroundProcesses.ElevatedServer.Projection\DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj", "{092AC740-DA01-4872-8E93-B9557DAD6BE5}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DevHome.QuietBackgroundProcesses.Server", "tools\QuietBackgroundProcesses\DevHome.QuietBackgroundProcesses.Server\DevHome.QuietBackgroundProcesses.Server.vcxproj", "{80805B43-CE75-4C6E-92F8-F385C1039E53}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DevHome.QuietBackgroundProcesses.Common", "tools\QuietBackgroundProcesses\DevHome.QuietBackgroundProcesses.Common\DevHome.QuietBackgroundProcesses.Common.vcxproj", "{4B370E2F-FB1D-4887-90BF-3B72517485CE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.QuietBackgroundProcesses.UI", "tools\QuietBackgroundProcesses\DevHome.QuietBackgroundProcesses.UI\DevHome.QuietBackgroundProcesses.UI.csproj", "{1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{DCAF188B-60C3-4EDB-8049-BAA927FBCD7D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SampleTool", "SampleTool", "{E7C94F61-D6CF-464D-8D50-210488AF7A50}" @@ -454,6 +465,86 @@ Global {2F9AD5AF-EF3B-496A-8566-9E9539E3DF43}.Release|x64.Build.0 = Release|x64 {2F9AD5AF-EF3B-496A-8566-9E9539E3DF43}.Release|x86.ActiveCfg = Release|x86 {2F9AD5AF-EF3B-496A-8566-9E9539E3DF43}.Release|x86.Build.0 = Release|x86 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Debug|Any CPU.ActiveCfg = Debug|x64 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Debug|Any CPU.Build.0 = Debug|x64 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Debug|arm64.ActiveCfg = Debug|ARM64 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Debug|arm64.Build.0 = Debug|ARM64 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Debug|x64.ActiveCfg = Debug|x64 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Debug|x64.Build.0 = Debug|x64 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Debug|x86.ActiveCfg = Debug|Win32 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Debug|x86.Build.0 = Debug|Win32 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Release|Any CPU.ActiveCfg = Release|x64 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Release|Any CPU.Build.0 = Release|x64 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Release|arm64.ActiveCfg = Release|ARM64 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Release|arm64.Build.0 = Release|ARM64 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Release|x64.ActiveCfg = Release|x64 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Release|x64.Build.0 = Release|x64 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Release|x86.ActiveCfg = Release|Win32 + {75945141-03AC-4C40-A586-16D463A0AC1B}.Release|x86.Build.0 = Release|Win32 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Debug|Any CPU.ActiveCfg = Debug|x64 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Debug|Any CPU.Build.0 = Debug|x64 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Debug|arm64.ActiveCfg = Debug|ARM64 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Debug|arm64.Build.0 = Debug|ARM64 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Debug|x64.ActiveCfg = Debug|x64 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Debug|x64.Build.0 = Debug|x64 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Debug|x86.ActiveCfg = Debug|x86 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Debug|x86.Build.0 = Debug|x86 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Release|Any CPU.ActiveCfg = Release|x64 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Release|Any CPU.Build.0 = Release|x64 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Release|arm64.ActiveCfg = Release|ARM64 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Release|arm64.Build.0 = Release|ARM64 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Release|x64.ActiveCfg = Release|x64 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Release|x64.Build.0 = Release|x64 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Release|x86.ActiveCfg = Release|x86 + {092AC740-DA01-4872-8E93-B9557DAD6BE5}.Release|x86.Build.0 = Release|x86 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Debug|Any CPU.ActiveCfg = Debug|x64 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Debug|Any CPU.Build.0 = Debug|x64 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Debug|arm64.ActiveCfg = Debug|ARM64 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Debug|arm64.Build.0 = Debug|ARM64 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Debug|x64.ActiveCfg = Debug|x64 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Debug|x64.Build.0 = Debug|x64 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Debug|x86.ActiveCfg = Debug|Win32 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Debug|x86.Build.0 = Debug|Win32 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Release|Any CPU.ActiveCfg = Release|x64 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Release|Any CPU.Build.0 = Release|x64 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Release|arm64.ActiveCfg = Release|ARM64 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Release|arm64.Build.0 = Release|ARM64 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Release|x64.ActiveCfg = Release|x64 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Release|x64.Build.0 = Release|x64 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Release|x86.ActiveCfg = Release|Win32 + {80805B43-CE75-4C6E-92F8-F385C1039E53}.Release|x86.Build.0 = Release|Win32 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Debug|Any CPU.ActiveCfg = Debug|x64 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Debug|Any CPU.Build.0 = Debug|x64 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Debug|arm64.ActiveCfg = Debug|ARM64 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Debug|arm64.Build.0 = Debug|ARM64 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Debug|x64.ActiveCfg = Debug|x64 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Debug|x64.Build.0 = Debug|x64 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Debug|x86.ActiveCfg = Debug|Win32 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Debug|x86.Build.0 = Debug|Win32 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|Any CPU.ActiveCfg = Release|x64 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|Any CPU.Build.0 = Release|x64 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|arm64.ActiveCfg = Release|ARM64 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|arm64.Build.0 = Release|ARM64 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|x64.ActiveCfg = Release|x64 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|x64.Build.0 = Release|x64 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|x86.ActiveCfg = Release|Win32 + {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|x86.Build.0 = Release|Win32 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|Any CPU.ActiveCfg = Debug|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|Any CPU.Build.0 = Debug|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|arm64.ActiveCfg = Debug|arm64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|arm64.Build.0 = Debug|arm64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|x64.ActiveCfg = Debug|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|x64.Build.0 = Debug|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|x86.ActiveCfg = Debug|x86 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|x86.Build.0 = Debug|x86 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|Any CPU.ActiveCfg = Release|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|Any CPU.Build.0 = Release|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|arm64.ActiveCfg = Release|arm64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|arm64.Build.0 = Release|arm64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|x64.ActiveCfg = Release|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|x64.Build.0 = Release|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|x86.ActiveCfg = Release|x86 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|x86.Build.0 = Release|x86 {CFD8A90D-8B6D-4ED6-BA35-FF894BEB46C0}.Debug|Any CPU.ActiveCfg = Debug|x64 {CFD8A90D-8B6D-4ED6-BA35-FF894BEB46C0}.Debug|Any CPU.Build.0 = Debug|x64 {CFD8A90D-8B6D-4ED6-BA35-FF894BEB46C0}.Debug|arm64.ActiveCfg = Debug|ARM64 @@ -700,6 +791,12 @@ Global {69F8B7DF-F52B-4B74-9A16-AB3241BB8912} = {F6EAB7D3-8F0A-4455-8969-2EF4A67314A0} {F6EAB7D3-8F0A-4455-8969-2EF4A67314A0} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} {2F9AD5AF-EF3B-496A-8566-9E9539E3DF43} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} + {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} + {75945141-03AC-4C40-A586-16D463A0AC1B} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} + {092AC740-DA01-4872-8E93-B9557DAD6BE5} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} + {80805B43-CE75-4C6E-92F8-F385C1039E53} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} + {4B370E2F-FB1D-4887-90BF-3B72517485CE} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} {E7C94F61-D6CF-464D-8D50-210488AF7A50} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} {8FC9A04E-1FFD-42BA-B304-D1FA964D99CE} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} {CFD8A90D-8B6D-4ED6-BA35-FF894BEB46C0} = {8FC9A04E-1FFD-42BA-B304-D1FA964D99CE} diff --git a/Directory.CppBuild.props b/Directory.CppBuild.props new file mode 100644 index 0000000000..280febbe6d --- /dev/null +++ b/Directory.CppBuild.props @@ -0,0 +1,8 @@ + + + $(Platform) + x86 + $(SolutionDir)tools\bin\$(CppPlatformTarget)\$(Configuration)\ + $(CppBaseOutDir)$(MSBuildProjectName)\ + + \ No newline at end of file diff --git a/common/DevHome.Common.csproj b/common/DevHome.Common.csproj index f2c411809a..3cf6850d84 100644 --- a/common/DevHome.Common.csproj +++ b/common/DevHome.Common.csproj @@ -34,7 +34,7 @@ - + diff --git a/settings/DevHome.Settings/Strings/en-us/Resources.resw b/settings/DevHome.Settings/Strings/en-us/Resources.resw index b1dc31a39e..c1dcbd8a1b 100644 --- a/settings/DevHome.Settings/Strings/en-us/Resources.resw +++ b/settings/DevHome.Settings/Strings/en-us/Resources.resw @@ -558,4 +558,12 @@ Environments Configuration Title text for the Environments configuration feature. + + Quiet background processes experiment + Name of experimental feature ()'Quiet Background Processes') on the 'Settings -> Experiments' page where you enable it. + + + Silence and track background processes that may hinder device performance + Inline description of the Quiet Background Processes experimental feature on the 'Settings -> Experiments' page where you enable it. + \ No newline at end of file diff --git a/src/DevHome.csproj b/src/DevHome.csproj index 0d127cc0df..77c167e1f9 100644 --- a/src/DevHome.csproj +++ b/src/DevHome.csproj @@ -3,6 +3,7 @@ + WinExe @@ -60,6 +61,7 @@ + all @@ -75,6 +77,10 @@ + + + + @@ -103,9 +109,31 @@ true + + + + <_DevHomeInternal_CppPlatform>$(Platform) + <_DevHomeInternal_CppPlatform Condition="'$(Platform)' == 'x86'">Win32 + $(DevHomeInternal_Win32RuntimePath)$(_DevHomeInternal_CppPlatform)\ + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + $(DefineConstants);CANARY_BUILD $(DefineConstants);STABLE_BUILD + diff --git a/src/NavConfig.jsonc b/src/NavConfig.jsonc index 867f9da0d2..4d570e97dc 100644 --- a/src/NavConfig.jsonc +++ b/src/NavConfig.jsonc @@ -32,6 +32,14 @@ "viewFullName": "DevHome.Environments.Views.LandingPage", "viewModelFullName": "DevHome.Environments.ViewModels.LandingPageViewModel", "icon": "E83B" + }, + { + "identity": "DevHome.QuietBackgroundProcesses.UI", + "assembly": "DevHome.QuietBackgroundProcesses.UI", + "viewFullName": "DevHome.QuietBackgroundProcesses.UI.Views.QuietBackgroundProcessesPage", + "viewModelFullName": "DevHome.QuietBackgroundProcesses.UI.ViewModels.QuietBackgroundProcessesViewModel", + "icon": "f5b0", + "experimentalFeatureIdentity": "QuietBackgroundProcessesExperiment" } ] } @@ -79,6 +87,27 @@ "visible": false } ] + }, + { + "identity": "QuietBackgroundProcessesExperiment", + "enabledByDefault": false, + "buildTypeOverrides": [ + { + "buildType": "dev", + "enabledByDefault": false, + "visible": true + }, + { + "buildType": "canary", + "enabledByDefault": false, + "visible": false + }, + { + "buildType": "release", + "enabledByDefault": false, + "visible": false + } + ] } ] } diff --git a/src/Package.appxmanifest b/src/Package.appxmanifest index 9fccbea3e8..49ef44fb3e 100644 --- a/src/Package.appxmanifest +++ b/src/Package.appxmanifest @@ -1,5 +1,5 @@  - + @@ -8,6 +8,22 @@ + + + + DevHome.QuietBackgroundProcesses.Server.exe + singleInstance + + + + + + + DevHome.QuietBackgroundProcesses.ElevatedServer.exe + singleInstance + + + diff --git a/test/DevHome.Test.csproj b/test/DevHome.Test.csproj index 08396c0ef9..2dc7b91664 100644 --- a/test/DevHome.Test.csproj +++ b/test/DevHome.Test.csproj @@ -1,5 +1,5 @@  - + DevHome.Test x86;x64;arm64 diff --git a/tools/Dashboard/DevHome.Dashboard.UnitTest/DevHome.Dashboard.UnitTest.csproj b/tools/Dashboard/DevHome.Dashboard.UnitTest/DevHome.Dashboard.UnitTest.csproj index ccf2b90e3a..dade52d4e8 100644 --- a/tools/Dashboard/DevHome.Dashboard.UnitTest/DevHome.Dashboard.UnitTest.csproj +++ b/tools/Dashboard/DevHome.Dashboard.UnitTest/DevHome.Dashboard.UnitTest.csproj @@ -1,5 +1,5 @@  - + Dashboard.Test x86;x64;arm64 diff --git a/tools/Experiments/src/DevHome.Experiments.csproj b/tools/Experiments/src/DevHome.Experiments.csproj index 461a8c3b2c..7dc9b769da 100644 --- a/tools/Experiments/src/DevHome.Experiments.csproj +++ b/tools/Experiments/src/DevHome.Experiments.csproj @@ -1,5 +1,5 @@  - + DevHome.Experiments x86;x64;arm64 @@ -12,6 +12,7 @@ + diff --git a/tools/Experiments/src/Strings/en-us/Resources.resw b/tools/Experiments/src/Strings/en-us/Resources.resw index 02a5ec5771..18553d4f5e 100644 --- a/tools/Experiments/src/Strings/en-us/Resources.resw +++ b/tools/Experiments/src/Strings/en-us/Resources.resw @@ -1,69 +1,69 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 - - - This is a test experiment page - The name is Experiments - - - Test Experiment page - The name of the Experiments page - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + This is a test experiment page + The name is Experiments + + + Test Experiment page + The name of the Experiments page + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/.gitattributes b/tools/QuietBackgroundProcesses/.gitattributes new file mode 100644 index 0000000000..5dbd6676cc --- /dev/null +++ b/tools/QuietBackgroundProcesses/.gitattributes @@ -0,0 +1,3 @@ +# Set default behavior to automatically normalize line endings. + +* text=crlf diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.Common.vcxproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.Common.vcxproj new file mode 100644 index 0000000000..9e1e35bf1c --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.Common.vcxproj @@ -0,0 +1,195 @@ + + + + C++ + + + + + true + true + true + true + {4B370E2F-FB1D-4887-90BF-3B72517485CE} + DevHome.QuietBackgroundProcesses.Common + DevHome.QuietBackgroundProcesses + en-US + 14.0 + true + Windows Store + 10.0 + 10.0.22000.0 + 10.0.17134.0 + false + AnyCPU\$(Configuration)\Merged\ + $(CppOutDir) + + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + StaticLibrary + v143 + v142 + v141 + v140 + Unicode + false + + + true + true + + + false + true + false + + + true + + + + + + + + + + + + + + + + NotUsing + + + + + Level4 + %(AdditionalOptions) /bigobj + _WINRT_DLL;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + + + Windows + false + + + + + + + stdcpp17 + %(AdditionalIncludeDirectories) + + + onecore.lib;%(AdditionalDependencies) + + + + + %(Filename).h + $(ProjectDir)$(GeneratedFilesDir)midl + $(ProjectDir)$(GeneratedFilesDir)winmd\%(Filename).winmd + + + + + _DEBUG;%(PreprocessorDefinitions) + Disabled + + + + + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + + + + + + + + + + + <_MdmergeInputs Include="@(Midl)" Condition="'%(Midl.ExcludedFromBuild)' != 'true'"> + %(Midl.MetadataFileName) + + + + + AnyCPU\$(Configuration)\Merged\ + -n:2 + -v + $(MdmergeParameters) -metadata_dir "$(WindowsSDK_MetadataFoundationPath)" + $(MdmergeParameters) @(MdmergeInputs->'-i "%(Identity)"', ' +') + $(MdmergeParameters) -o "$(MdmergeMergedDir.TrimEnd('\'))" -partial $(MdmergeMergeDepth) + + + $(MdmergeMergedDir)DevHome.QuietBackgroundProcesses.winmd + $(IntDir)$(MSBuildProjectFile).mdmerge.rsp + $(MdMergePath)mdmerge %40"$(MdmergeResponseFile)" + + + + + + + + + copy /Y "$(MdmergeMergedDir)DevHome.QuietBackgroundProcesses.winmd" "$(OutDir)." + + + + + + + + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.idl b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.idl new file mode 100644 index 0000000000..c64cb8d568 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.idl @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import "inspectable.idl"; + +namespace DevHome.QuietBackgroundProcesses +{ + [default_interface] + runtimeclass QuietBackgroundProcessesSession + { + static QuietBackgroundProcessesSession GetSingleton(); + Int64 Start(); + void Stop(); + Boolean IsActive { get; }; + Int64 TimeLeftInSeconds { get; }; + } + + [default_interface] + runtimeclass QuietBackgroundProcessesSessionManager { + QuietBackgroundProcessesSessionManager(); + static Boolean IsFeaturePresent(); + static QuietBackgroundProcessesSession GetSession(); + static QuietBackgroundProcessesSession TryGetSession(); + } +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/PropertySheet.props b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/PropertySheet.props new file mode 100644 index 0000000000..e34141b019 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/PropertySheet.props @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/Utility.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/Utility.h new file mode 100644 index 0000000000..9b42fa2fad --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/Utility.h @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +constexpr bool DEBUG_BUILD = +#if _DEBUG + true; +#else + false; +#endif + +template +struct wrl_module_object_ref +{ + struct details + { + static void wrl_decrement_object_count() + { + auto& module = T::GetModule(); + auto count = module.DecrementObjectCount(); + auto msg = std::wstring(L"WRL: DecrementObjectCount = ") + std::to_wstring(count) + std::wstring(L"\n"); + OutputDebugStringW(msg.c_str()); + } + }; + + using wrl_module_object_ref_releaser = wil::unique_call; + + wrl_module_object_ref() + { + auto& module = T::GetModule(); + auto count = module.IncrementObjectCount(); + auto msg = std::wstring(L"WRL: IncrementObjectCount = ") + std::to_wstring(count) + std::wstring(L"\n"); + OutputDebugStringW(msg.c_str()); + + m_moduleReference.activate(); + } + + wrl_module_object_ref(wrl_module_object_ref&& other) noexcept = default; + wrl_module_object_ref& operator=(wrl_module_object_ref&& other) noexcept = default; + + wrl_module_object_ref(const wrl_module_object_ref&) = delete; + wrl_module_object_ref& operator=(const wrl_module_object_ref&) = delete; + + void reset() + { + m_moduleReference.reset(); + } + +private: + + wrl_module_object_ref_releaser m_moduleReference{ false }; +}; + +using wrl_server_process_ref = wrl_module_object_ref>; + +inline std::optional try_get_registry_value_dword(HKEY key, _In_opt_ PCWSTR subKey, _In_opt_ PCWSTR value_name, ::wil::reg::key_access access = ::wil::reg::key_access::read) +{ + wil::unique_hkey hkey; + if (SUCCEEDED(wil::reg::open_unique_key_nothrow(key, subKey, hkey, access))) + { + if (auto keyvalue = wil::reg::try_get_value_dword(hkey.get(), value_name)) + { + return keyvalue.value(); + } + } + return std::nullopt; +} + +inline void WaitForDebuggerIfPresent() +{ + auto waitForDebugger = try_get_registry_value_dword(HKEY_LOCAL_MACHINE, LR"(Software\Microsoft\Windows\CurrentVersion\DevHome\QuietBackgroundProcesses)", L"WaitForDebugger"); + + if (waitForDebugger.value_or(0)) + { + while (!IsDebuggerPresent()) + { + Sleep(1000); + }; + DebugBreak(); + } +} + +inline bool IsTokenElevated(HANDLE token) +{ + auto mandatoryLabel = wil::get_token_information(token); + LONG levelRid = static_cast(mandatoryLabel->Label.Sid)->SubAuthority[0]; + return levelRid == SECURITY_MANDATORY_HIGH_RID; +} + +inline void SelfElevate(std::optional const& arguments) +{ + auto path = wil::GetModuleFileNameW(); + + SHELLEXECUTEINFO sei = { sizeof(sei) }; + sei.lpVerb = L"runas"; + sei.lpFile = path.get(); + sei.lpParameters = arguments.value().c_str(); + sei.hwnd = NULL; + sei.nShow = SW_NORMAL; + + THROW_LAST_ERROR_IF(!ShellExecuteEx(&sei)); +} + +inline std::wstring ParseServerNameArgument(std::wstring_view wargv) +{ + constexpr wchar_t serverNamePrefix[] = L"-ServerName:"; + if (_wcsnicmp(wargv.data(), serverNamePrefix, wcslen(serverNamePrefix)) != 0) + { + THROW_HR(E_UNEXPECTED); + } + return { wargv.data() + wcslen(serverNamePrefix) }; +} + +inline void SetComFastRundownAndNoEhHandle() +{ + // Enable fast rundown of COM stubs in this process to ensure that RPCSS bookkeeping is updated synchronously. + wil::com_ptr pGlobalOptions; + THROW_IF_FAILED(CoCreateInstance(CLSID_GlobalOptions, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pGlobalOptions))); + THROW_IF_FAILED(pGlobalOptions->Set(COMGLB_RO_SETTINGS, COMGLB_FAST_RUNDOWN)); + THROW_IF_FAILED(pGlobalOptions->Set(COMGLB_EXCEPTION_HANDLING, COMGLB_EXCEPTION_DONOT_HANDLE_ANY)); +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/empty.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/empty.cpp new file mode 100644 index 0000000000..2c3629c084 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/empty.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// A cpp is required to produce a .lib, and a .lib is required to exist if this project is +// referenced by another cpp project. \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj new file mode 100644 index 0000000000..f1922205f0 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj @@ -0,0 +1,36 @@ + + + + + + + enable + enable + $(CppBaseOutDir)\DevHome.QuietBackgroundProcesses.Common\ + + + + + 10.0.19041.0 + DevHome.QuietBackgroundProcesses + $(OutDir) + + + + $(OutDir) + + + + + + + + + + + + + + + + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj new file mode 100644 index 0000000000..1dfb5c237d --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj @@ -0,0 +1,173 @@ + + + + C++ + + + + + + true + true + true + true + {75945141-03ac-4c40-a586-16d463a0ac1b} + DevHome.QuietBackgroundProcesses.ElevatedServer + DevHome.QuietBackgroundProcesses + en-US + 14.0 + true + Windows Store + 10.0 + 10.0.22000.0 + 10.0.17134.0 + false + true + $(CppOutDir) + + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + $(CppOutDir) + + + Application + v143 + v142 + v141 + v140 + Unicode + false + + + true + true + + + false + true + false + + + + + + + + + + + + + + + + + Use + pch.h + $(IntDir)pch.pch + Level4 + %(AdditionalOptions) /bigobj + _WINRT_DLL;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + + + Windows + false + + + + + + + stdcpp17 + $(ProjectDir)..\DevHome.QuietBackgroundProcesses.Common\;$(ProjectDir)..\DevHome.QuietBackgroundProcesses.Common\$(GeneratedFilesDir)midl;%(AdditionalIncludeDirectories) + + + onecore.lib;%(AdditionalDependencies) + + + + + _DEBUG;%(PreprocessorDefinitions) + Disabled + + + + + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + + + + + Create + + + + + + + + + + + + + {4b370e2f-fb1d-4887-90bf-3b72517485ce} + + + + + + + + + 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/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/PropertySheet.props b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/PropertySheet.props new file mode 100644 index 0000000000..e34141b019 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/PropertySheet.props @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietBackgroundProcessesSession.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietBackgroundProcessesSession.cpp new file mode 100644 index 0000000000..fbd08ff74a --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietBackgroundProcessesSession.cpp @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include "TimedQuietSession.h" + +#include "DevHome.QuietBackgroundProcesses.h" + +constexpr auto DEFAULT_QUIET_DURATION = std::chrono::hours(2); + +std::mutex g_mutex; +std::unique_ptr g_activeTimer; + +namespace ABI::DevHome::QuietBackgroundProcesses +{ + class QuietBackgroundProcessesSession : + public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, + IQuietBackgroundProcessesSession, + Microsoft::WRL::FtmBase> + { + InspectableClass(RuntimeClass_DevHome_QuietBackgroundProcesses_QuietBackgroundProcessesSession, BaseTrust); + + public: + STDMETHODIMP RuntimeClassInitialize() noexcept + { + return S_OK; + } + + // IQuietBackgroundProcessesSession + STDMETHODIMP Start(__int64* result) noexcept override try + { + auto lock = std::scoped_lock(g_mutex); + + // Stop and discard the previous timer + if (g_activeTimer) + { + g_activeTimer->Cancel(); + } + + std::chrono::seconds duration = DEFAULT_QUIET_DURATION; + if (auto durationOverride = try_get_registry_value_dword(HKEY_LOCAL_MACHINE, LR"(Software\Microsoft\Windows\CurrentVersion\DevHome\QuietBackgroundProcesses)", L"Duration")) + { + duration = std::chrono::seconds(durationOverride.value()); + } + + // Start timer + g_activeTimer.reset(new TimedQuietSession(duration)); + + // Return duration for showing countdown + *result = g_activeTimer->TimeLeftInSeconds(); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP Stop() noexcept override try + { + auto lock = std::scoped_lock(g_mutex); + + // Turn off quiet mode and cancel timer + if (g_activeTimer) + { + g_activeTimer->Cancel(); + g_activeTimer.reset(); + } + + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_IsActive(::boolean* value) noexcept override try + { + auto lock = std::scoped_lock(g_mutex); + *value = false; + if (g_activeTimer) + { + *value = g_activeTimer->IsActive(); + } + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_TimeLeftInSeconds(__int64* value) noexcept override try + { + auto lock = std::scoped_lock(g_mutex); + *value = 0; + if (g_activeTimer) + { + *value = g_activeTimer->TimeLeftInSeconds(); + } + return S_OK; + } + CATCH_RETURN() + }; + + class QuietBackgroundProcessesSessionStatics WrlFinal : + public Microsoft::WRL::AgileActivationFactory< + Microsoft::WRL::Implements> + { + InspectableClassStatic(RuntimeClass_DevHome_QuietBackgroundProcesses_QuietBackgroundProcessesSession, BaseTrust); + + public: + STDMETHODIMP ActivateInstance(_COM_Outptr_ IInspectable**) noexcept + { + // Disallow activation - must use GetSingleton() + return E_NOTIMPL; + } + + // IQuietBackgroundProcessesSessionStatics + STDMETHODIMP GetSingleton(_COM_Outptr_ IQuietBackgroundProcessesSession** session) noexcept override try + { + // Instanced objects are the only feasible way to manage a COM singleton without keeping a strong + // handle to the server - which keeps it alive. (IWeakReference keeps a strong handle to the server!) + // An 'instance' can be thought of as a 'handle' to 'the singleton' backend. + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(session)); + return S_OK; + } + CATCH_RETURN() + }; + + ActivatableClassWithFactory(QuietBackgroundProcessesSession, QuietBackgroundProcessesSessionStatics); +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietState.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietState.cpp new file mode 100644 index 0000000000..b6a6fbf14d --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietState.cpp @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" + +#include + +#include +#include "QuietState.h" + +namespace QuietState +{ + std::mutex g_mutex; + + void TurnOff() noexcept + { + auto lock = std::scoped_lock(g_mutex); + LOG_IF_FAILED(DisableQuietBackgroundProcesses()); + } + + unique_quietwindowclose_call TurnOn() + { + auto lock = std::scoped_lock(g_mutex); + THROW_IF_FAILED(EnableQuietBackgroundProcesses()); + return unique_quietwindowclose_call{}; + } +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietState.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietState.h new file mode 100644 index 0000000000..92ac8855c7 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietState.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +#pragma once + +#include + +namespace QuietState +{ + void TurnOff() noexcept; + + using unique_quietwindowclose_call = wil::unique_call; + + unique_quietwindowclose_call TurnOn(); +} + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/TimedQuietSession.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/TimedQuietSession.h new file mode 100644 index 0000000000..3dcc157e69 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/TimedQuietSession.h @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include "DevHome.QuietBackgroundProcesses.h" + +#include "Timer.h" +#include "QuietState.h" + +using ElevatedServerReference = wrl_server_process_ref; + +struct UnelevatedServerReference +{ + wil::com_ptr m_reference; + + UnelevatedServerReference() : + m_reference(wil::GetActivationFactory(RuntimeClass_DevHome_QuietBackgroundProcesses_QuietBackgroundProcessesSessionManager)) + { + } + + void reset() + { + m_reference.reset(); + } +}; + + +// TimedQuietSession is a 2 hour "Quiet Background Processes" timed window that disables quiet +// mode when the timer expires or when explicitly cancelled. It keeps also keeps the server alive. +// +// TimedQuietSession maintains, +// 1. quiet mode signal +// 2. session timer +// 3. handle to session (elevated) server +// 4. handle to manager (unelevated) server +// +// COM server lifetime & process interaction: +// +// Processes: +// DevHome (client) -> UnelevatedServer (manager) -> ElevatedServer (session) +// +// Manager (unelevated server): +// Role of manager is simply allow client to check if session (elevated server) is alive +// without risking launching a UAC prompt. It caches a com proxy interface to session. +// +// Session (elevated server): +// Role of session is to enable/disable quiet mode and keep a timer. +// +// Lifetime: +// - The manager and session keep each other alive. +// +// - The session (TimedQuietSession) runs in ElevatedServer and keeps this elevated server +// alive until timer expiration. +// +// - The session (TimedQuietSession) also keeps the manager (in unelevated server) alive until timer expiration; +// this is only because the manager caches a *strong* handle to the session (elevated server), +// and there is no way to invalidate the session (proxy handle) in the client if we tore down +// this session. (Using IWeakReference does hold a weak reference to the object, but also holds a strong +// reference against the hosting process's lifetime.) +// +// Teardown Sequence: +// When all session timers (elevated) expire, the manager (unelevated) reference is released -> COM triggers +// teardown in unelevated server (assuming DevHome is closed), releasing the cached *strong* reference +// to the session (elevated) -> COM triggers teardown in elevated server. +// +struct TimedQuietSession +{ + TimedQuietSession(std::chrono::seconds seconds) + { + m_timer = std::make_unique(seconds, [this]() { + auto lock = std::scoped_lock(m_mutex); + Deactivate(); + }); + + // Turn on quiet mode + m_quietState = QuietState::TurnOn(); + } + + TimedQuietSession(TimedQuietSession&& other) noexcept = default; + TimedQuietSession& operator=(TimedQuietSession&& other) noexcept = default; + + TimedQuietSession(const TimedQuietSession&) = delete; + TimedQuietSession& operator=(const TimedQuietSession&) = delete; + + int64_t TimeLeftInSeconds() + { + auto lock = std::scoped_lock(m_mutex); + return m_timer->TimeLeftInSeconds(); + } + + bool IsActive() + { + auto lock = std::scoped_lock(m_mutex); + return (bool)m_quietState; + } + + void Cancel() + { + auto lock = std::scoped_lock(m_mutex); + + Deactivate(); + m_timer->Cancel(); + + // Destruct timer on another thread because it's destructor is blocking + auto destructionThread = std::thread([timer = std::move(m_timer)]() { + // destruct timer here + }); + + destructionThread.detach(); + } + +private: + void Deactivate() + { + // Turn off quiet mode + m_quietState.reset(); + + // Release lifetime handles to this elevated server and unelevated client server + m_unelevatedServer.reset(); + m_elevatedServer.reset(); + } + + UnelevatedServerReference m_unelevatedServer; // Manager server + ElevatedServerReference m_elevatedServer; // Session server (this server) + + QuietState::unique_quietwindowclose_call m_quietState{ false }; + std::unique_ptr m_timer; + std::mutex m_mutex; +}; diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Timer.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Timer.h new file mode 100644 index 0000000000..f65699da0d --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Timer.h @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "pch.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Utility.h" + +#if _DEBUG || NDEBUG +#define TRACK_SECONDS_LEFT +#endif + +class Timer +{ +public: + Timer(std::chrono::seconds seconds, std::function callback) : + m_callback(std::forward>(callback)), + m_startTime(std::chrono::steady_clock::now()), + m_duration(seconds), + m_timerThread(std::thread(&Timer::TimerThread, this)) + { + } + + Timer(Timer&& other) noexcept = default; + Timer& operator=(Timer&& other) noexcept = default; + + Timer(const Timer&) = delete; + Timer& operator=(const Timer&) = delete; + + ~Timer() + { + if (m_timerThread.joinable()) + { + m_timerThread.join(); + } + } + + void Cancel() + { + OutputDebugStringW(L"Timer: Cancelling\n"); + + auto lock = std::scoped_lock(m_mutex); + + // Disable the callback from being called... + m_cancelled = true; + + // ... wake up the timer thread. + m_cancelCondition.notify_one(); + } + + int64_t TimeLeftInSeconds() + { + auto lock = std::scoped_lock(m_mutex); + if (m_cancelled) + { + return 0; + } + + auto secondsLeft = CalculateSecondsLeft(); + return std::max(secondsLeft, 0ll); + } + +private: + int64_t CalculateSecondsLeft() + { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - m_startTime); + auto secondsLeft = m_duration.count() - elapsed.count(); +#ifdef TRACK_SECONDS_LEFT + m_secondsLeft = secondsLeft; +#endif + return secondsLeft; + } + + void TimerThread() + { + // Pause until timer expired or cancelled + auto lock = std::unique_lock(m_mutex); + + m_cancelCondition.wait_for(lock, m_duration, [this] { + return this->m_cancelled; + }); + + // Do the callback + if (!this->m_cancelled) + { + this->m_callback(); + } + + OutputDebugStringW(L"Timer: Finished\n"); + } + + std::chrono::steady_clock::time_point m_startTime{}; + std::chrono::seconds m_duration{}; + std::thread m_timerThread; + std::mutex m_mutex; + bool m_cancelled{}; + std::condition_variable m_cancelCondition; + std::function m_callback; + +#ifdef TRACK_SECONDS_LEFT + int64_t m_secondsLeft = -1; +#endif +}; diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/main.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/main.cpp new file mode 100644 index 0000000000..067085996e --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/main.cpp @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "Utility.h" +#include "TimedQuietSession.h" +#include "QuietState.h" + +int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR wargv, int wargc) try +{ + constexpr auto ELEVATED_SERVER_STARTED_EVENT_NAME = L"Global\\DevHome_QuietBackgroundProcesses_ElevatedServer_Started"; + + if (wargc < 1) + { + THROW_HR(E_INVALIDARG); + } + + // Parse the servername from the cmdline argument, e.g. "-ServerName:DevHome.QuietBackgroundProcesses.ElevatedServer" + auto serverName = ParseServerNameArgument(wargv); + + if (wil::compare_string_ordinal(serverName, L"DevHome.QuietBackgroundProcesses.ElevatedServer", true) != 0) + { + THROW_HR(E_INVALIDARG); + } + + // Let's self-elevate and terminate + if (!IsTokenElevated(GetCurrentProcessToken())) + { + wil::unique_event elevatedServerRunningEvent; + elevatedServerRunningEvent.create(wil::EventOptions::ManualReset, ELEVATED_SERVER_STARTED_EVENT_NAME); + + // Launch elevated instance + SelfElevate(wargv); + + // Wait for the *actual* elevated server instance to register its winrt classes with COM before shutting down + elevatedServerRunningEvent.wait(); + return 0; + } + + WaitForDebuggerIfPresent(); + + auto unique_rouninitialize_call = wil::RoInitialize(); + + // Enable fast rundown of COM stubs in this process to ensure that RPCSS bookkeeping is updated synchronously. + SetComFastRundownAndNoEhHandle(); + + // To be safe, force quiet mode off to begin the proceedings in case we leaked the machine state previously + QuietState::TurnOff(); + + std::mutex mutex; + bool comFinished{}; + std::condition_variable finishCondition; + +#pragma warning(push) +#pragma warning(disable : 4324) // Avoid WRL alignment warning + + // Register WRL callback when all objects are destroyed + auto& module = Microsoft::WRL::Module::Create([&] { + // The last instance object of the module is released + { + auto lock = std::unique_lock(mutex); + comFinished = true; + } + finishCondition.notify_one(); + }); + +#pragma warning(pop) + + // Register WinRT activatable classes + module.RegisterObjects(); + auto unique_wrl_registration_cookie = wil::scope_exit([&module]() { + module.UnregisterObjects(); + }); + + // Tell the unelevated server instance that we've registered our winrt classes with COM (so it can terminate) + wil::unique_event elevatedServerRunningEvent; + elevatedServerRunningEvent.open(ELEVATED_SERVER_STARTED_EVENT_NAME); + elevatedServerRunningEvent.SetEvent(); + + // Wait for all server references to release (implicitly also waiting for timers to finish via CoAddRefServerProcess) + auto lock = std::unique_lock(mutex); + + finishCondition.wait(lock, [&] { + return comFinished; + }); + + // Safety + QuietState::TurnOff(); + + return 0; +} +CATCH_RETURN() diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/packages.config b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/packages.config new file mode 100644 index 0000000000..42b20a75de --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/pch.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/pch.cpp new file mode 100644 index 0000000000..d58e93e60c --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +#include "pch.h" diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/pch.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/pch.h new file mode 100644 index 0000000000..601ace8be4 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/pch.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +#pragma once + +#define NOMINMAX + +#include +#include +#include diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/DevHome.QuietBackgroundProcesses.Server.vcxproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/DevHome.QuietBackgroundProcesses.Server.vcxproj new file mode 100644 index 0000000000..492cee0113 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/DevHome.QuietBackgroundProcesses.Server.vcxproj @@ -0,0 +1,175 @@ + + + + C++ + + + + + + true + true + true + true + {80805B43-CE75-4C6E-92F8-F385C1039E53} + DevHome.QuietBackgroundProcesses.Server + DevHome.QuietBackgroundProcesses + en-US + 14.0 + true + Windows Store + 10.0 + 10.0.22000.0 + 10.0.17134.0 + false + $(CppOutDir) + + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + Application + v143 + v142 + v141 + v140 + Unicode + false + + + true + true + + + false + true + false + + + true + + + + + + + + + + + + + + + + + Use + pch.h + $(IntDir)pch.pch + Level4 + %(AdditionalOptions) /bigobj + _WINRT_DLL;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + + + Windows + false + + + + + + + stdcpp17 + C++ + $(ProjectDir)..\DevHome.QuietBackgroundProcesses.Common\;$(ProjectDir)..\DevHome.QuietBackgroundProcesses.Common\$(GeneratedFilesDir)midl;%(AdditionalIncludeDirectories) + + + onecore.lib;%(AdditionalDependencies) + + + %(Filename).h + $(ProjectDir)$(GeneratedFilesDir)midl + $(ProjectDir)$(GeneratedFilesDir)winmd\%(Filename).winmd + + + + + _DEBUG;%(PreprocessorDefinitions) + Disabled + + + + + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + + Create + + + + + + + + + + + + {4b370e2f-fb1d-4887-90bf-3b72517485ce} + + + + + + + + + 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/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/PropertySheet.props b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/PropertySheet.props new file mode 100644 index 0000000000..e34141b019 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/PropertySheet.props @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/QuietBackgroundProcessesSessionManager.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/QuietBackgroundProcessesSessionManager.cpp new file mode 100644 index 0000000000..8f5a76b7cd --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/QuietBackgroundProcessesSessionManager.cpp @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +#include "DevHome.QuietBackgroundProcesses.h" + +namespace ABI::DevHome::QuietBackgroundProcesses +{ + class QuietBackgroundProcessesSessionManager : + public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, + IQuietBackgroundProcessesSessionManager, + Microsoft::WRL::FtmBase> + { + InspectableClass(RuntimeClass_DevHome_QuietBackgroundProcesses_QuietBackgroundProcessesSessionManager, BaseTrust); + + public: + STDMETHODIMP RuntimeClassInitialize() noexcept + { + return S_OK; + } + }; + + class QuietBackgroundProcessesSessionManagerStatics WrlFinal : + public Microsoft::WRL::AgileActivationFactory< + Microsoft::WRL::Implements> + { + InspectableClassStatic(RuntimeClass_DevHome_QuietBackgroundProcesses_QuietBackgroundProcessesSessionManager, BaseTrust); + + public: + // IActivationFactory method + STDMETHODIMP ActivateInstance(_Outptr_result_nullonfailure_ IInspectable** ppvObject) noexcept + try + { + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(ppvObject)); + return S_OK; + } + CATCH_RETURN() + + // IQuietBackgroundProcessesSessionManagerStatics + STDMETHODIMP IsFeaturePresent(_Out_ boolean* isPresent) noexcept override try + { + THROW_IF_FAILED(IsQuietBackgroundProcessesFeaturePresent((bool*)isPresent)); + return S_OK; + } + CATCH_RETURN(); + + STDMETHODIMP GetSession(_Outptr_result_nullonfailure_ IQuietBackgroundProcessesSession** session) noexcept override + try + { + auto lock = std::scoped_lock(m_mutex); + *session = nullptr; + + if (!m_sessionReference) + { + auto factory = wil::GetActivationFactory(RuntimeClass_DevHome_QuietBackgroundProcesses_QuietBackgroundProcessesSession); + THROW_IF_FAILED(factory->GetSingleton(&m_sessionReference)); + } + m_sessionReference.copy_to(session); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP TryGetSession(_COM_Outptr_result_maybenull_ IQuietBackgroundProcessesSession** session) noexcept override try + { + auto lock = std::scoped_lock(m_mutex); + m_sessionReference.try_copy_to(session); + return S_OK; + } + CATCH_RETURN() + + private: + std::mutex m_mutex; + wil::com_ptr m_sessionReference; + }; + + ActivatableClassWithFactory(QuietBackgroundProcessesSessionManager, QuietBackgroundProcessesSessionManagerStatics); +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/main.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/main.cpp new file mode 100644 index 0000000000..54ae2b766b --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/main.cpp @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "Utility.h" + +int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR wargv, int wargc) try +{ + WaitForDebuggerIfPresent(); + + if (wargc < 1) + { + THROW_HR(E_INVALIDARG); + } + + // Parse the servername from the cmdline argument, e.g. "-ServerName:DevHome.QuietBackgroundProcesses.Server" + auto serverName = ParseServerNameArgument(wargv); + + if (wil::compare_string_ordinal(serverName, L"DevHome.QuietBackgroundProcesses.Server", true) != 0) + { + THROW_HR(E_INVALIDARG); + } + + auto unique_rouninitialize_call = wil::RoInitialize(); + + // Enable fast rundown of COM stubs in this process to ensure that RPCSS bookkeeping is updated synchronously. + SetComFastRundownAndNoEhHandle(); + + std::mutex mutex; + bool comFinished{}; + std::condition_variable finishCondition; + +#pragma warning(push) +#pragma warning(disable: 4324) // Avoid WRL alignment warning + + // Register WRL callback when all objects are destroyed + auto& module = Microsoft::WRL::Module::Create([&] { + // The last instance object of the module is released + { + auto lock = std::unique_lock(mutex); + comFinished = true; + } + finishCondition.notify_one(); + }); + +#pragma warning(pop) + + // Register WinRT activatable classes + module.RegisterObjects(); + auto unique_wrl_registration_cookie = wil::scope_exit([&module]() { + module.UnregisterObjects(); + }); + + // Wait for all server references to release + auto lock = std::unique_lock(mutex); + + finishCondition.wait(lock, [&] { + return comFinished; + }); + + return 0; +} +CATCH_RETURN() diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/packages.config b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/packages.config new file mode 100644 index 0000000000..28332c3d1a --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/pch.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/pch.cpp new file mode 100644 index 0000000000..40e691ba78 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/pch.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/pch.h new file mode 100644 index 0000000000..9d6cb89293 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/pch.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#define NOMINMAX + +#include +#include +#include diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/DevHome.QuietBackgroundProcesses.UI.csproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/DevHome.QuietBackgroundProcesses.UI.csproj new file mode 100644 index 0000000000..2e8e6ecd31 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/DevHome.QuietBackgroundProcesses.UI.csproj @@ -0,0 +1,22 @@ + + + + DevHome.QuietBackgroundProcesses.UI + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + true + + + + + + + + + + + + $(DefaultXamlRuntime) + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw new file mode 100644 index 0000000000..bcc14801bd --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Quiet Background Processes + The name of the Quiet Background Processes page + + + Silence and track background processes that may hinder device performance + Description of the Quiet Background Processes feature + + + Quiet background processes + Inline title of the Quiet Background Processes feature + + + This feature can be activated for 2 hours. + A description of the Quiet Background Processes time window + + + Quiet background processes beta feature description + Title of the next settings card that explains the feature in more detail + + + Link to docs + Link that launches documentation + + + Provide feedback + Link that launches feedback + + + Related links + Label for the doc links + + + + + Feature not supported on this version of Windows + Indicates that this OS isn't new enough to support the feature + + + Session Error + Something went wrong when running the session + + + Session ended + The quiet session was cancelled or the time expired + + + Unable to cancel session + Something went wrong when cancelling the session + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs new file mode 100644 index 0000000000..9387a34d2c --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Helpers; +using DevHome.Common.Services; +using Microsoft.UI.Xaml; + +namespace DevHome.QuietBackgroundProcesses.UI.ViewModels; + +public partial class QuietBackgroundProcessesViewModel : ObservableObject +{ + private readonly bool _isFeaturePresent; + private readonly TimeSpan _zero; +#nullable enable + private DevHome.QuietBackgroundProcesses.QuietBackgroundProcessesSession? _session; +#nullable disable + + [ObservableProperty] + private string _sessionStateText; + + private DevHome.QuietBackgroundProcesses.QuietBackgroundProcessesSession GetSession() + { + if (_session == null) + { + _session = QuietBackgroundProcessesSessionManager.GetSession(); + } + + return _session; + } + + private string GetString(string id) + { + var stringResource = new StringResource("DevHome.QuietBackgroundProcesses.UI/Resources"); + return stringResource.GetLocalized(id); + } + + private string GetStatusString(string id) + { + return GetString("QuietBackgroundProcesses_Status_" + id); + } + + public QuietBackgroundProcessesViewModel() + { + _zero = new TimeSpan(0, 0, 0); + + _isFeaturePresent = DevHome.QuietBackgroundProcesses.QuietBackgroundProcessesSessionManager.IsFeaturePresent(); + if (!_isFeaturePresent) + { + SessionStateText = GetStatusString("FeatureNotSupported"); + return; + } + + // Resume countdown if there's an existing quiet window + // + // Note: GetIsActive() won't ever launch a UAC prompt, but GetTimeRemaining() will if no session is running - so be careful with call order + if (GetIsActive()) + { + _isToggleOn = true; + var timeLeftInSeconds = GetTimeRemaining(); + StartCountdownTimer(timeLeftInSeconds); + } + } + + public bool IsToggleEnabled => _isFeaturePresent; + + private bool _isToggleOn; + + public bool IsToggleOn + { + get => _isToggleOn; + + set + { + if (_isToggleOn == value) + { + return; + } + + _isToggleOn = value; + + // Stop any existing timer + _dispatcherTimer?.Stop(); + + if (_isToggleOn) + { + try + { + // Launch the server, which then elevates itself, showing a UAC prompt + var timeLeftInSeconds = GetSession().Start(); + StartCountdownTimer(timeLeftInSeconds); + } + catch (Exception ex) + { + SessionStateText = GetStatusString("SessionError"); + Log.Logger()?.ReportError("QuietBackgroundProcessesSession::Start failed", ex); + } + } + else + { + try + { + GetSession().Stop(); + SessionStateText = GetStatusString("SessionEnded"); + } + catch (Exception ex) + { + SessionStateText = GetStatusString("UnableToCancelSession"); + Log.Logger()?.ReportError("QuietBackgroundProcessesSession::Stop failed", ex); + } + } + } + } + + private bool GetIsActive() + { + try + { + _session = DevHome.QuietBackgroundProcesses.QuietBackgroundProcessesSessionManager.TryGetSession(); + if (_session != null) + { + return _session.IsActive; + } + } + catch (Exception ex) + { + SessionStateText = GetStatusString("SessionError"); + Log.Logger()?.ReportError("QuietBackgroundProcessesSession::IsActive failed", ex); + } + + return false; + } + + private int GetTimeRemaining() + { + try + { + return (int)GetSession().TimeLeftInSeconds; + } + catch (Exception ex) + { + SessionStateText = GetStatusString("SessionError"); + Log.Logger()?.ReportError("QuietBackgroundProcessesSession::TimeLeftInSeconds failed", ex); + return 0; + } + } + + private DispatcherTimer _dispatcherTimer; + private TimeSpan _secondsLeft; + + private void StartCountdownTimer(long timeLeftInSeconds) + { + if (timeLeftInSeconds <= 0) + { + return; + } + + _dispatcherTimer = new DispatcherTimer(); + _dispatcherTimer.Tick += DispatcherTimer_Tick; + _dispatcherTimer.Interval = new TimeSpan(0, 0, 1); + _secondsLeft = new TimeSpan(0, 0, (int)timeLeftInSeconds); + _dispatcherTimer.Start(); + + SessionStateText = _secondsLeft.ToString(); + } + + private void DispatcherTimer_Tick(object sender, object e) + { + var sessionEnded = false; + + _secondsLeft = new TimeSpan(0, 0, GetTimeRemaining()); + + if (_secondsLeft.CompareTo(_zero) <= 0) + { + // The window should be closed, but let's confirm with the server + if (GetSession().IsActive) + { + // There has been some drift + _secondsLeft = new TimeSpan(0, 0, GetTimeRemaining()); + } + else + { + _dispatcherTimer.Stop(); + _secondsLeft = _zero; + IsToggleOn = false; + sessionEnded = true; + } + } + + if (sessionEnded) + { + SessionStateText = GetStatusString("SessionEnded"); + } + else + { + SessionStateText = _secondsLeft.ToString(); // CultureInfo.InvariantCulture + } + } +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesPage.xaml b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesPage.xaml new file mode 100644 index 0000000000..ffcdf746b4 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesPage.xaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesPage.xaml.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesPage.xaml.cs new file mode 100644 index 0000000000..9f0fb77e41 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesPage.xaml.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common; +using DevHome.QuietBackgroundProcesses.UI.ViewModels; + +namespace DevHome.QuietBackgroundProcesses.UI.Views; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class QuietBackgroundProcessesPage : ToolPage +{ + public override string ShortName => "Quiet Background Processes"; + + public QuietBackgroundProcessesViewModel ViewModel + { + get; + } + + public QuietBackgroundProcessesPage() + { + ViewModel = new QuietBackgroundProcessesViewModel(); + InitializeComponent(); + } +} From 2ea4a98c8d8c96fe0d3683a65ad60a8a469ad487 Mon Sep 17 00:00:00 2001 From: subhasan Date: Thu, 14 Mar 2024 13:15:07 -0700 Subject: [PATCH 003/121] Adding Telemetry to measure if users looks for more Apps in app install list (#2410) Co-authored-by: Subha Santhanam --- .../DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs index f3604806e5..bcf3d14b07 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs @@ -14,6 +14,7 @@ using DevHome.SetupFlow.Behaviors; using DevHome.SetupFlow.Common.Helpers; using DevHome.SetupFlow.Services; +using DevHome.Telemetry; using Microsoft.UI.Dispatching; namespace DevHome.SetupFlow.ViewModels; @@ -138,6 +139,7 @@ private void RemoveShimmers(int count) [RelayCommand] private void ViewAllPackages(PackageCatalogViewModel catalog) { + TelemetryFactory.Get().LogCritical("Apps_ViewAll_Event"); AppManagementBehavior.SetHeaderVisibility(false); ViewAllCatalog = catalog; } From db1ce51fdc0d4ff25c899e7f77023026c7329e61 Mon Sep 17 00:00:00 2001 From: Eric Johnson Date: Fri, 15 Mar 2024 09:27:04 -0700 Subject: [PATCH 004/121] Fix experimental features visibility in nav menu (#2420) --- src/Views/ShellPage.xaml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Views/ShellPage.xaml.cs b/src/Views/ShellPage.xaml.cs index e8d7e875fe..6e4a690a6f 100644 --- a/src/Views/ShellPage.xaml.cs +++ b/src/Views/ShellPage.xaml.cs @@ -149,12 +149,12 @@ public void UpdateExperimentalPageState(ExperimentalFeature expFeature) private void UpdateNavigationMenuItems() { - var expVM = App.Current.GetService(); + var expService = App.Current.GetService(); foreach (var group in App.NavConfig.NavMenu.Groups) { foreach (var tool in group.Tools) { - var expFeature = expVM.ExperimentalFeatures.FirstOrDefault(x => x.Id == tool.ExperimentalFeatureIdentity); + var expFeature = expService.ExperimentalFeatures.FirstOrDefault(x => x.Id == tool.ExperimentalFeatureIdentity); var navigationViewItemString = $@" Date: Fri, 15 Mar 2024 14:19:20 -0700 Subject: [PATCH 005/121] Add telemetry for Page Next Source (#2413) * 2412 Adding instrumentation to understand BAckup restore flow * Updating file names * Renaming Telemetry file to help better understand the scenario being tracked. --------- Co-authored-by: Subha Santhanam --- .../SetupFlow/PageNextSourceEvent.cs | 31 +++++++++++++++++++ .../ViewModels/SetupPageViewModelBase.cs | 3 ++ 2 files changed, 34 insertions(+) create mode 100644 common/TelemetryEvents/SetupFlow/PageNextSourceEvent.cs diff --git a/common/TelemetryEvents/SetupFlow/PageNextSourceEvent.cs b/common/TelemetryEvents/SetupFlow/PageNextSourceEvent.cs new file mode 100644 index 0000000000..ff1b0cd003 --- /dev/null +++ b/common/TelemetryEvents/SetupFlow/PageNextSourceEvent.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Tracing; +using DevHome.Telemetry; +using Microsoft.Diagnostics.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; + +namespace DevHome.Common.TelemetryEvents.SetupFlow; + +[EventData] +public class PageNextSourceEvent : EventBase +{ + public string NavigationTarget + { + get; + } + + public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage; + + public PageNextSourceEvent(string navigationTarget) + { + NavigationTarget = navigationTarget; + } + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + // No sensitive strings to replace. + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs index 20b1bdca83..4442a845e0 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.TelemetryEvents.SetupFlow; using DevHome.SetupFlow.Common.Helpers; using DevHome.SetupFlow.Services; +using DevHome.Telemetry; namespace DevHome.SetupFlow.ViewModels; @@ -138,6 +140,7 @@ public async Task OnNavigateFromAsync() { _hasExecutedFirstNavigateFrom = true; Log.Logger?.ReportInfo(Log.Component.Orchestrator, $"Executing pre-navigation tasks for page {this.GetType().Name}"); + TelemetryFactory.Get().Log("PageNavigated", LogLevel.Critical, new PageNextSourceEvent(this.GetType().Name)); await OnFirstNavigateFromAsync(); } } From 7845c1ccfad7e7c49adc4cda70b86c357af80342 Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Mon, 18 Mar 2024 11:07:02 -0400 Subject: [PATCH 006/121] Provide API for registering nested pages (#2356) --- common/Services/IPageService.cs | 17 ++++++++++++ docs/tools.md | 16 +++++------- docs/tools/Dashboard.md | 14 ++++++++++ .../Extensions/PageExtensions.cs | 24 +++++++++++++++++ src/Contracts/Services/IPageService.cs | 9 ------- src/Services/NavigationService.cs | 1 - src/Services/PageService.cs | 26 ++++++++----------- .../Extensions/PageExtensions.cs | 16 ++++++++++++ 8 files changed, 88 insertions(+), 35 deletions(-) create mode 100644 common/Services/IPageService.cs create mode 100644 docs/tools/Dashboard.md create mode 100644 settings/DevHome.Settings/Extensions/PageExtensions.cs delete mode 100644 src/Contracts/Services/IPageService.cs create mode 100644 tools/ExtensionLibrary/DevHome.ExtensionLibrary/Extensions/PageExtensions.cs diff --git a/common/Services/IPageService.cs b/common/Services/IPageService.cs new file mode 100644 index 0000000000..1354b09a31 --- /dev/null +++ b/common/Services/IPageService.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Common.Services; + +public interface IPageService +{ + Type GetPageType(string key); + + public void Configure() + where T_VM : ObservableObject + where T_V : Page; +} diff --git a/docs/tools.md b/docs/tools.md index 3b0654215b..4768b49eed 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -42,7 +42,7 @@ The Dev Home framework will look at all types in its assembly for any inheriting On a found type, the framework will use: - ShortName property to get the name of the tool -### Method definition +#### Method definition This section contains a more detailed description of each of the interface methods. @@ -59,16 +59,12 @@ Returns the name of the tool. This is used for the navigation menu text. [`toolpage.cs`](../common/ToolPage.cs) Contains the interface definition for Dev Home tools. -## Dashboard Tool -The Dashboard page hosts and displays Windows Widgets. Widgets are small UI containers that display text and graphics, associated with an app installed on the device. For information on creating widgets, see [Widgets Overview](https://learn.microsoft.com/windows/apps/design/widgets/) and [Widget providers](https://learn.microsoft.com/windows/apps/develop/widgets/widget-providers). +### Navigation -Each widget is represented by a [`WidgetViewModel`](../tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs). The WidgetViewModel displays itself inside a [`WidgetControl`](../tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml), and the WidgetControls are grouped into a [`WidgetBoard`](../tools/Dashboard/DevHome.Dashboard/Controls/WidgetBoard.cs). +In order to allow navigation to your tool, your page and ViewModel must be registered with the PageService. If your tool only contains one page, it is automatically registered for you since you added your page to `NavConfig.jsonc`. However, you may have other sub-pages you wish to register. -### Widget UI +In order to do so, you must create an extension method for the PageService inside your tool. See examples in [Settings](../settings/DevHome.Settings/Extensions/PageExtensions.cs) or [Extensions](../tools/ExtensionLibrary/DevHome.ExtensionLibrary/Extensions/PageExtensions.cs). Then, call your extension from the [PageService](../src/Services/PageService.cs). -The Widget UI consists of two main parts. At the top, there is a context menu and an attribution area. For more information on these components, please read [Built-in widget UI components](https://learn.microsoft.com/windows/apps/design/widgets/widgets-states-and-ui#built-in-widget-ui-components). The rest of the widget content is an [Adaptive Card](https://learn.microsoft.com/windows/apps/design/widgets/widgets-create-a-template) provided by the [Widget Provider](https://learn.microsoft.com/windows/apps/develop/widgets/widget-providers). +## Existing tools -Widgets are rendered by Adaptive Cards, and there are a few ways Dev Home customizes the look and feel of the cards. Please note all of these are subject to change while Dev Home is in Preview. -* Dev Home widgets use the [Adaptive Card schema](https://adaptivecards.io/explorer/) version 1.5, which is the most recent schema supported by the WinUI 3 Adaptive Card renderer. -* There are [HostConfig](https://learn.microsoft.com/adaptive-cards/sdk/rendering-cards/uwp/host-config) files that define common styles (e.g., font family, font sizes, default spacing) and behaviors (e.g., max number of actions) for all the widgets. There is one for [light mode](../tools/Dashboard/DevHome.Dashboard/Assets/HostConfigLight.json) and one for [dark mode](../tools/Dashboard/DevHome.Dashboard/Assets/HostConfigDark.json). -* Dev Home supports a custom AdaptiveElement type called [`LabelGroup`](../common/Renderers/LabelGroup.cs). This allows a card author to render a set of labels, each with a specified background color. For an example of how to use this type, please see the [GitHub Issues widget](https://github.com/microsoft/devhomegithubextension/blob/main/src/GitHubExtension/Widgets/Templates/GitHubIssuesTemplate.json). +[Dashboard](./tools/Dashboard.md) diff --git a/docs/tools/Dashboard.md b/docs/tools/Dashboard.md new file mode 100644 index 0000000000..d6cc48f007 --- /dev/null +++ b/docs/tools/Dashboard.md @@ -0,0 +1,14 @@ +# Dashboard + +The Dashboard page hosts and displays Windows Widgets. Widgets are small UI containers that display text and graphics, associated with an app installed on the device. For information on creating widgets, see [Widgets Overview](https://learn.microsoft.com/windows/apps/design/widgets/) and [Widget providers](https://learn.microsoft.com/windows/apps/develop/widgets/widget-providers). + +Each widget is represented by a [`WidgetViewModel`](../tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs). The WidgetViewModel displays itself inside a [`WidgetControl`](../../tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml), and the WidgetControls are grouped into a [`WidgetBoard`](../../tools/Dashboard/DevHome.Dashboard/Controls/WidgetBoard.cs). + +### Widget UI + +The Widget UI consists of two main parts. At the top, there is a context menu and an attribution area. For more information on these components, please read [Built-in widget UI components](https://learn.microsoft.com/windows/apps/design/widgets/widgets-states-and-ui#built-in-widget-ui-components). The rest of the widget content is an [Adaptive Card](https://learn.microsoft.com/windows/apps/design/widgets/widgets-create-a-template) provided by the [Widget Provider](https://learn.microsoft.com/windows/apps/develop/widgets/widget-providers). + +Widgets are rendered by Adaptive Cards, and there are a few ways Dev Home customizes the look and feel of the cards. Please note all of these are subject to change while Dev Home is in Preview. +* Dev Home widgets use the [Adaptive Card schema](https://adaptivecards.io/explorer/) version 1.5, which is the most recent schema supported by the WinUI 3 Adaptive Card renderer. +* There are [HostConfig](https://learn.microsoft.com/adaptive-cards/sdk/rendering-cards/uwp/host-config) files that define common styles (e.g., font family, font sizes, default spacing) and behaviors (e.g., max number of actions) for all the widgets. There is one for [light mode](../../tools/Dashboard/DevHome.Dashboard/Assets/HostConfigLight.json) and one for [dark mode](../tools/Dashboard/DevHome.Dashboard/Assets/HostConfigDark.json). +* Dev Home supports a custom AdaptiveElement type called [`LabelGroup`](../../common/Renderers/LabelGroup.cs). This allows a card author to render a set of labels, each with a specified background color. For an example of how to use this type, please see the [GitHub Issues widget](https://github.com/microsoft/devhomegithubextension/blob/main/src/GitHubExtension/Widgets/Templates/GitHubIssuesTemplate.json). diff --git a/settings/DevHome.Settings/Extensions/PageExtensions.cs b/settings/DevHome.Settings/Extensions/PageExtensions.cs new file mode 100644 index 0000000000..ae134dd85f --- /dev/null +++ b/settings/DevHome.Settings/Extensions/PageExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Services; +using DevHome.Settings.ViewModels; +using DevHome.Settings.Views; + +namespace DevHome.Settings.Extensions; + +public static class PageExtensions +{ + public static void ConfigureSettingsPages(this IPageService pageService) + { + // Settings is not a Tool, so the main page is not configured automatically. Configure it here. + pageService.Configure(); + + // Configure sub-pages + pageService.Configure(); + pageService.Configure(); + pageService.Configure(); + pageService.Configure(); + pageService.Configure(); + } +} diff --git a/src/Contracts/Services/IPageService.cs b/src/Contracts/Services/IPageService.cs deleted file mode 100644 index d1725a9e9f..0000000000 --- a/src/Contracts/Services/IPageService.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace DevHome.Contracts.Services; - -public interface IPageService -{ - Type GetPageType(string key); -} diff --git a/src/Services/NavigationService.cs b/src/Services/NavigationService.cs index 90ce9ebf0d..6347b8e9d1 100644 --- a/src/Services/NavigationService.cs +++ b/src/Services/NavigationService.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using DevHome.Common.Services; -using DevHome.Contracts.Services; using DevHome.Contracts.ViewModels; using DevHome.Dashboard.ViewModels; using DevHome.Helpers; diff --git a/src/Services/PageService.cs b/src/Services/PageService.cs index bcdccabf38..dbe3a3e8a4 100644 --- a/src/Services/PageService.cs +++ b/src/Services/PageService.cs @@ -5,11 +5,8 @@ using DevHome.Common.Contracts; using DevHome.Common.Models; using DevHome.Common.Services; -using DevHome.Contracts.Services; -using DevHome.ExtensionLibrary.ViewModels; -using DevHome.ExtensionLibrary.Views; -using DevHome.Settings.ViewModels; -using DevHome.Settings.Views; +using DevHome.ExtensionLibrary.Extensions; +using DevHome.Settings.Extensions; using DevHome.ViewModels; using DevHome.Views; using Microsoft.UI.Xaml.Controls; @@ -30,15 +27,7 @@ public class PageService : IPageService public PageService(ILocalSettingsService localSettingsService, IExperimentationService experimentationService) { - Configure(); - Configure(); - Configure(); - Configure(); - Configure(); - Configure(); - Configure(); - Configure(); - + // Configure top-level pages from registered tools var assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var group in App.NavConfig.NavMenu.Groups) { @@ -52,6 +41,13 @@ where assembly.GetName().Name == tool.Assembly } } + // Configure other pages + Configure(); + + this.ConfigureExtensionLibraryPages(); + this.ConfigureSettingsPages(); + + // Configure Experimental Feature pages ExperimentalFeature.LocalSettingsService = localSettingsService; foreach (var experimentalFeature in App.NavConfig.ExperimentFeatures ?? Array.Empty()) { @@ -85,7 +81,7 @@ public Type GetPageType(string key) return pageType; } - private void Configure() + public void Configure() where T_VM : ObservableObject where T_V : Page { diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Extensions/PageExtensions.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Extensions/PageExtensions.cs new file mode 100644 index 0000000000..07131df620 --- /dev/null +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Extensions/PageExtensions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Services; +using DevHome.ExtensionLibrary.ViewModels; +using DevHome.ExtensionLibrary.Views; + +namespace DevHome.ExtensionLibrary.Extensions; + +public static class PageExtensions +{ + public static void ConfigureExtensionLibraryPages(this IPageService pageService) + { + pageService.Configure(); + } +} From 57e1a328f02fcd22c15ce6dc777e66f9f023adba Mon Sep 17 00:00:00 2001 From: Jeff Whiteside Date: Mon, 18 Mar 2024 11:05:30 -0700 Subject: [PATCH 007/121] Quiet Background Processes: Changed toggle to button (#2411) * Quiet Background Processes: Changed toggle to button Minor: Also stopped spamming server each second for clock update. --- .../Strings/en-us/Resources.resw | 8 ++ .../QuietBackgroundProcessesViewModel.cs | 132 ++++++++++-------- .../Views/QuietBackgroundProcessesPage.xaml | 14 +- 3 files changed, 87 insertions(+), 67 deletions(-) diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw index bcc14801bd..69698e90ac 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw @@ -62,6 +62,14 @@ Quiet Background Processes The name of the Quiet Background Processes page + + Start session + Button that starts a quiet background session + + + Stop session + Button that stops a quiet background session + Silence and track background processes that may hinder device performance Description of the Quiet Background Processes feature diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs index 9387a34d2c..bc7eb79b2a 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs @@ -3,6 +3,7 @@ using System; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using DevHome.Common.Helpers; using DevHome.Common.Services; using Microsoft.UI.Xaml; @@ -11,15 +12,24 @@ namespace DevHome.QuietBackgroundProcesses.UI.ViewModels; public partial class QuietBackgroundProcessesViewModel : ObservableObject { - private readonly bool _isFeaturePresent; - private readonly TimeSpan _zero; + private readonly TimeSpan _zero = new TimeSpan(0, 0, 0); + private readonly TimeSpan _oneSecond = new TimeSpan(0, 0, 1); #nullable enable private DevHome.QuietBackgroundProcesses.QuietBackgroundProcessesSession? _session; #nullable disable + [ObservableProperty] + private bool _isFeaturePresent; + [ObservableProperty] private string _sessionStateText; + [ObservableProperty] + private bool _quietButtonChecked; + + [ObservableProperty] + private string _quietButtonText; + private DevHome.QuietBackgroundProcesses.QuietBackgroundProcessesSession GetSession() { if (_session == null) @@ -43,72 +53,70 @@ private string GetStatusString(string id) public QuietBackgroundProcessesViewModel() { - _zero = new TimeSpan(0, 0, 0); + IsFeaturePresent = DevHome.QuietBackgroundProcesses.QuietBackgroundProcessesSessionManager.IsFeaturePresent(); - _isFeaturePresent = DevHome.QuietBackgroundProcesses.QuietBackgroundProcessesSessionManager.IsFeaturePresent(); - if (!_isFeaturePresent) + var running = false; + if (IsFeaturePresent) + { + // Check if an existing quiet session is running. + // Note: GetIsActive() won't ever launch a UAC prompt, but GetTimeRemaining() will if no session is running - so be careful with call order + running = GetIsActive(); + } + else { SessionStateText = GetStatusString("FeatureNotSupported"); - return; } // Resume countdown if there's an existing quiet window - // - // Note: GetIsActive() won't ever launch a UAC prompt, but GetTimeRemaining() will if no session is running - so be careful with call order - if (GetIsActive()) - { - _isToggleOn = true; - var timeLeftInSeconds = GetTimeRemaining(); - StartCountdownTimer(timeLeftInSeconds); - } + SetQuietSessionRunningState(running); } - public bool IsToggleEnabled => _isFeaturePresent; + private void SetQuietSessionRunningState(bool running, long? timeLeftInSeconds = null) + { + if (running) + { + var seconds = timeLeftInSeconds ?? GetTimeRemaining(); + StartCountdownTimer(seconds); + QuietButtonText = GetString("QuietBackgroundProcesses_QuietButton_Stop"); + } + else + { + _dispatcherTimer?.Stop(); + QuietButtonText = GetString("QuietBackgroundProcesses_QuietButton_Start"); + } - private bool _isToggleOn; + QuietButtonChecked = !running; + } - public bool IsToggleOn + [RelayCommand] + public void QuietButtonClicked() { - get => _isToggleOn; - - set + if (QuietButtonChecked) { - if (_isToggleOn == value) + try { - return; + // Launch the server, which then elevates itself, showing a UAC prompt + var timeLeftInSeconds = GetSession().Start(); + SetQuietSessionRunningState(true, timeLeftInSeconds); } - - _isToggleOn = value; - - // Stop any existing timer - _dispatcherTimer?.Stop(); - - if (_isToggleOn) + catch (Exception ex) + { + SessionStateText = GetStatusString("SessionError"); + Log.Logger()?.ReportError("QuietBackgroundProcessesSession::Start failed", ex); + } + } + else + { + try { - try - { - // Launch the server, which then elevates itself, showing a UAC prompt - var timeLeftInSeconds = GetSession().Start(); - StartCountdownTimer(timeLeftInSeconds); - } - catch (Exception ex) - { - SessionStateText = GetStatusString("SessionError"); - Log.Logger()?.ReportError("QuietBackgroundProcessesSession::Start failed", ex); - } + GetSession().Stop(); + SetQuietSessionRunningState(false); + SessionStateText = GetStatusString("SessionEnded"); } - else + catch (Exception ex) { - try - { - GetSession().Stop(); - SessionStateText = GetStatusString("SessionEnded"); - } - catch (Exception ex) - { - SessionStateText = GetStatusString("UnableToCancelSession"); - Log.Logger()?.ReportError("QuietBackgroundProcessesSession::Stop failed", ex); - } + SessionStateText = GetStatusString("UnableToCancelSession"); + Log.Logger()?.ReportError("QuietBackgroundProcessesSession::Stop failed", ex); } } } @@ -167,29 +175,29 @@ private void StartCountdownTimer(long timeLeftInSeconds) private void DispatcherTimer_Tick(object sender, object e) { - var sessionEnded = false; + // Subtract 1 second + _secondsLeft = _secondsLeft.Subtract(_oneSecond); - _secondsLeft = new TimeSpan(0, 0, GetTimeRemaining()); + // Every 2 minutes ask the server for the actual time remaining (to resolve any drift) + if (_secondsLeft.Seconds % 120 == 0) + { + _secondsLeft = new TimeSpan(0, 0, GetTimeRemaining()); + } + var sessionEnded = false; if (_secondsLeft.CompareTo(_zero) <= 0) { // The window should be closed, but let's confirm with the server - if (GetSession().IsActive) - { - // There has been some drift - _secondsLeft = new TimeSpan(0, 0, GetTimeRemaining()); - } - else + if (!GetSession().IsActive) { - _dispatcherTimer.Stop(); - _secondsLeft = _zero; - IsToggleOn = false; sessionEnded = true; } } if (sessionEnded) { + SetQuietSessionRunningState(false); + _secondsLeft = _zero; SessionStateText = GetStatusString("SessionEnded"); } else diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesPage.xaml b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesPage.xaml index ffcdf746b4..e03a58cc51 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesPage.xaml +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesPage.xaml @@ -16,18 +16,22 @@ - + - - - + + + From cf539dc7c4cd2854ba6658cfbe1ce85078e879ab Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Mon, 18 Mar 2024 16:53:41 -0400 Subject: [PATCH 008/121] Make KnownPageKeys available for navigation (#2350) --- .../DevHome.Settings => common}/Models/Breadcrumb.cs | 2 +- common/Services/INavigationService.cs | 11 +++++++++++ docs/architecture.md | 2 -- docs/tools.md | 9 +++++++++ .../DevHome.Settings/ViewModels/AboutViewModel.cs | 2 +- .../DevHome.Settings/ViewModels/AccountsViewModel.cs | 2 +- .../DevHome.Settings/ViewModels/FeedbackViewModel.cs | 2 +- .../ViewModels/PreferencesViewModel.cs | 2 +- .../DevHome.Settings/ViewModels/SettingsViewModel.cs | 1 + settings/DevHome.Settings/Views/AccountsPage.xaml.cs | 2 +- .../DevHome.ExtensionLibrary.csproj | 1 - .../ViewModels/ExtensionLibraryViewModel.cs | 3 +-- .../ViewModels/ExtensionSettingsViewModel.cs | 2 +- .../DevHome.SetupFlow/DevHome.SetupFlow.csproj | 2 -- .../DevHome.SetupFlow/ViewModels/SummaryViewModel.cs | 6 ++---- 15 files changed, 31 insertions(+), 18 deletions(-) rename {settings/DevHome.Settings => common}/Models/Breadcrumb.cs (90%) diff --git a/settings/DevHome.Settings/Models/Breadcrumb.cs b/common/Models/Breadcrumb.cs similarity index 90% rename from settings/DevHome.Settings/Models/Breadcrumb.cs rename to common/Models/Breadcrumb.cs index 2662c50a77..4a9c50adce 100644 --- a/settings/DevHome.Settings/Models/Breadcrumb.cs +++ b/common/Models/Breadcrumb.cs @@ -5,7 +5,7 @@ using DevHome.Common.Services; using Microsoft.UI.Xaml; -namespace DevHome.Settings.Models; +namespace DevHome.Common.Models; public class Breadcrumb { diff --git a/common/Services/INavigationService.cs b/common/Services/INavigationService.cs index f7e87e8756..0a592fc9a9 100644 --- a/common/Services/INavigationService.cs +++ b/common/Services/INavigationService.cs @@ -37,3 +37,14 @@ string DefaultPage bool GoForward(); } + +// Expose known page keys so that a project doesn't need to include a ProjectReference to another project +// just to navigate to another page. +public static class KnownPageKeys +{ + public static readonly string Dashboard = "DevHome.Dashboard.ViewModels.DashboardViewModel"; + public static readonly string Extensions = "DevHome.ExtensionLibrary.ViewModels.ExtensionLibraryViewModel"; + public static readonly string WhatsNew = "DevHome.ViewModels.WhatsNewViewModel"; + public static readonly string Settings = "DevHome.Settings.ViewModels.SettingsViewModel"; + public static readonly string Feedback = "DevHome.Settings.ViewModels.FeedbackViewModel"; +} diff --git a/docs/architecture.md b/docs/architecture.md index 47612e7aef..fa3a4126e0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -28,8 +28,6 @@ graph TD; DevHome.SetupFlow.Common-->DevHome.SetupFlow.ElevatedServer; DevHome.SetupFlow.ElevatedComponent-->DevHome.SetupFlow.ElevatedServer; DevHome.SetupFlow.ElevatedComponent.Projection-->DevHome.SetupFlow; - DevHome.Dashboard-->DevHome.SetupFlow; - DevHome.Settings-->DevHome.SetupFlow; CoreWidgetProvider-->DevHome; DevHome.Dashboard-->DevHome; DevHome.Experiments-->DevHome; diff --git a/docs/tools.md b/docs/tools.md index 4768b49eed..5a76119de2 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -65,6 +65,15 @@ In order to allow navigation to your tool, your page and ViewModel must be regis In order to do so, you must create an extension method for the PageService inside your tool. See examples in [Settings](../settings/DevHome.Settings/Extensions/PageExtensions.cs) or [Extensions](../tools/ExtensionLibrary/DevHome.ExtensionLibrary/Extensions/PageExtensions.cs). Then, call your extension from the [PageService](../src/Services/PageService.cs). +#### Navigating away from your tool + +If you want a user action (such as clicking a link) to navigate away from your tool and your project does not otherwise rely on the destination project, do not create a PackageReference to the destination project from yours. Instead, add the destination page to `INavigationService.KnownPageKeys` and use it for navigation, like in this example: +```cs +navigationService.NavigateTo(KnownPageKeys.); +``` + +This keeps the dependency tree simple, and prevents circular references when two projects want to navigate to each other. + ## Existing tools [Dashboard](./tools/Dashboard.md) diff --git a/settings/DevHome.Settings/ViewModels/AboutViewModel.cs b/settings/DevHome.Settings/ViewModels/AboutViewModel.cs index 352d306876..4d482042c7 100644 --- a/settings/DevHome.Settings/ViewModels/AboutViewModel.cs +++ b/settings/DevHome.Settings/ViewModels/AboutViewModel.cs @@ -5,8 +5,8 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DevHome.Common.Extensions; +using DevHome.Common.Models; using DevHome.Common.Services; -using DevHome.Settings.Models; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; diff --git a/settings/DevHome.Settings/ViewModels/AccountsViewModel.cs b/settings/DevHome.Settings/ViewModels/AccountsViewModel.cs index a68d3e4b2a..b79a0a396a 100644 --- a/settings/DevHome.Settings/ViewModels/AccountsViewModel.cs +++ b/settings/DevHome.Settings/ViewModels/AccountsViewModel.cs @@ -8,8 +8,8 @@ using CommunityToolkit.Mvvm.Input; using DevHome.Common.Contracts.Services; using DevHome.Common.Extensions; +using DevHome.Common.Models; using DevHome.Common.Services; -using DevHome.Settings.Models; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; diff --git a/settings/DevHome.Settings/ViewModels/FeedbackViewModel.cs b/settings/DevHome.Settings/ViewModels/FeedbackViewModel.cs index 9609dbc2f7..ffcd31b38e 100644 --- a/settings/DevHome.Settings/ViewModels/FeedbackViewModel.cs +++ b/settings/DevHome.Settings/ViewModels/FeedbackViewModel.cs @@ -6,9 +6,9 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DevHome.Common.Extensions; +using DevHome.Common.Models; using DevHome.Common.Services; using DevHome.Contracts.Services; -using DevHome.Settings.Models; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; diff --git a/settings/DevHome.Settings/ViewModels/PreferencesViewModel.cs b/settings/DevHome.Settings/ViewModels/PreferencesViewModel.cs index 0d71a9399d..49ec130ada 100644 --- a/settings/DevHome.Settings/ViewModels/PreferencesViewModel.cs +++ b/settings/DevHome.Settings/ViewModels/PreferencesViewModel.cs @@ -5,9 +5,9 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Models; using DevHome.Common.Services; using DevHome.Contracts.Services; -using DevHome.Settings.Models; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; diff --git a/settings/DevHome.Settings/ViewModels/SettingsViewModel.cs b/settings/DevHome.Settings/ViewModels/SettingsViewModel.cs index 5fdbd839c1..79661087ee 100644 --- a/settings/DevHome.Settings/ViewModels/SettingsViewModel.cs +++ b/settings/DevHome.Settings/ViewModels/SettingsViewModel.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DevHome.Common.Extensions; +using DevHome.Common.Models; using DevHome.Common.Services; using DevHome.Settings.Models; using Microsoft.UI.Xaml; diff --git a/settings/DevHome.Settings/Views/AccountsPage.xaml.cs b/settings/DevHome.Settings/Views/AccountsPage.xaml.cs index 09a4e8cfec..40a51c18f5 100644 --- a/settings/DevHome.Settings/Views/AccountsPage.xaml.cs +++ b/settings/DevHome.Settings/Views/AccountsPage.xaml.cs @@ -66,7 +66,7 @@ private async void AddAccountButton_Click(object sender, RoutedEventArgs e) private void FindExtensions() { var navigationService = Application.Current.GetService(); - navigationService.NavigateTo("DevHome.ExtensionLibrary.ViewModels.ExtensionLibraryViewModel"); + navigationService.NavigateTo(KnownPageKeys.Extensions); } private async void AddDeveloperId_Click(object sender, RoutedEventArgs e) diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/DevHome.ExtensionLibrary.csproj b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/DevHome.ExtensionLibrary.csproj index ace6f4a2dd..3e4bc667bb 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/DevHome.ExtensionLibrary.csproj +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/DevHome.ExtensionLibrary.csproj @@ -19,7 +19,6 @@ - diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs index 2f9710adab..215b0c189e 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs @@ -12,7 +12,6 @@ using DevHome.Common.Extensions; using DevHome.Common.Services; using DevHome.ExtensionLibrary.Helpers; -using DevHome.Settings.ViewModels; using Microsoft.UI.Xaml; using Microsoft.Windows.DevHome.SDK; using Windows.ApplicationModel; @@ -208,6 +207,6 @@ public Visibility GetNoAvailablePackagesVisibility(int availablePackagesCount, b public void SendFeedbackClick() { var navigationService = Application.Current.GetService(); - _ = navigationService.NavigateTo(typeof(FeedbackViewModel).FullName!); + _ = navigationService.NavigateTo(KnownPageKeys.Feedback); } } diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs index 98064f92ea..94d7258199 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs @@ -6,10 +6,10 @@ using AdaptiveCards.Rendering.WinUI3; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Models; using DevHome.Common.Services; using DevHome.Common.Views; using DevHome.Logging; -using DevHome.Settings.Models; using Microsoft.UI.Xaml.Controls; using Microsoft.Windows.DevHome.SDK; diff --git a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj index a94b1854ac..13d2bf5221 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj @@ -26,8 +26,6 @@ - - Projection diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs index 219a3f0890..7e3e7156b3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs @@ -13,8 +13,6 @@ using DevHome.Common.Services; using DevHome.Common.TelemetryEvents.SetupFlow; using DevHome.Contracts.Services; -using DevHome.Dashboard.ViewModels; -using DevHome.Settings.ViewModels; using DevHome.SetupFlow.Common.Helpers; using DevHome.SetupFlow.Models; using DevHome.SetupFlow.Services; @@ -151,7 +149,7 @@ public void GoToMainPage() public void GoToDashboard() { TelemetryFactory.Get().Log("Summary_NavigateTo_Event", LogLevel.Critical, new NavigateFromSummaryEvent("Dashboard"), Orchestrator.ActivityId); - _host.GetService().NavigateTo(typeof(DashboardViewModel).FullName); + _host.GetService().NavigateTo(KnownPageKeys.Dashboard); _setupFlowViewModel.TerminateCurrentFlow("Summary_GoToDashboard"); } @@ -159,7 +157,7 @@ public void GoToDashboard() public void GoToDevHomeSettings() { TelemetryFactory.Get().Log("Summary_NavigateTo_Event", LogLevel.Critical, new NavigateFromSummaryEvent("DevHomeSettings"), Orchestrator.ActivityId); - _host.GetService().NavigateTo(typeof(SettingsViewModel).FullName); + _host.GetService().NavigateTo(KnownPageKeys.Settings); _setupFlowViewModel.TerminateCurrentFlow("Summary_GoToSettings"); } From 9c74073dc7f44371c083647506064238940b6dd1 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Tue, 19 Mar 2024 13:42:41 -0700 Subject: [PATCH 009/121] Adding IRepositoryProvider2 (#2430) * IDL * Updating Build version --------- Co-authored-by: Darren Hoehna --- build/azure-pipelines.yml | 2 +- .../Microsoft.Windows.DevHome.SDK.idl | 57 +++++++++++++++++- .../Microsoft.Windows.DevHome.SDK.vcxproj | 2 + .../RepositoriesResult.cpp | 1 + .../RepositoriesSearchResult.cpp | 58 +++++++++++++++++++ .../RepositoriesSearchResult.h | 32 ++++++++++ 6 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 extensionsdk/Microsoft.Windows.DevHome.SDK/RepositoriesSearchResult.cpp create mode 100644 extensionsdk/Microsoft.Windows.DevHome.SDK/RepositoriesSearchResult.h diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 76bd2ac4c3..1cead03cab 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -22,7 +22,7 @@ parameters: variables: # MSIXVersion's second part should always be odd to account for stub app's version MSIXVersion: '0.1201' - VersionOfSDK: '0.200' + VersionOfSDK: '0.300' solution: '**/DevHome.sln' appxPackageDir: 'AppxPackages' testOutputArtifactDir: 'TestResults' diff --git a/extensionsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.idl b/extensionsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.idl index e7b6b40ec8..7115dc1a66 100644 --- a/extensionsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.idl +++ b/extensionsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.idl @@ -1,6 +1,6 @@ namespace Microsoft.Windows.DevHome.SDK { - [contractversion(2)] + [contractversion(3)] apicontract DevHomeContract {} [contract(Microsoft.Windows.DevHome.SDK.DevHomeContract, 1)] @@ -77,6 +77,30 @@ namespace Microsoft.Windows.DevHome.SDK Windows.Foundation.IAsyncOperation CloneRepositoryAsync(IRepository repository, String cloneDestination, IDeveloperId developerId); }; + [contract(Microsoft.Windows.DevHome.SDK.DevHomeContract, 3)] interface IRepositoryProvider2 + requires IRepositoryProvider + { + String[] SearchFieldNames + { + get; + }; + + Boolean IsSearchingSupported + { + get; + }; + + String AskToSearchLabel + { + get; + }; + + // This should return a list of suggestions given fieldValues. + // "Suggestions" is dependant on the extension and how it keeps track of repositories. + Windows.Foundation.IAsyncOperation > GetValuesForSearchFieldAsync(IMapView fieldValues, String requestedSearchField, IDeveloperId developerId); + Windows.Foundation.IAsyncOperation GetRepositoriesAsync(IMapView fieldValues, IDeveloperId developerId); + }; + [contract(Microsoft.Windows.DevHome.SDK.DevHomeContract, 1)] runtimeclass RepositoryResult { RepositoryResult(IRepository repository); @@ -107,6 +131,37 @@ namespace Microsoft.Windows.DevHome.SDK }; }; + [contract(Microsoft.Windows.DevHome.SDK.DevHomeContract, 3)] runtimeclass RepositoriesSearchResult { + RepositoriesSearchResult(IIterable repositories); + RepositoriesSearchResult(IIterable repositories, String selectionsOptionsLabel, String[] selectionOptions, String selectionOptionsName); + RepositoriesSearchResult(HRESULT e, String diagnosticText); + + IIterable Repositories + { + get; + }; + + String SelectionOptionsLabel + { + get; + }; + + String[] SelectionOptions + { + get; + }; + + String SelectionOptionsName + { + get; + }; + + ProviderOperationResult Result + { + get; + }; + }; + [contract(Microsoft.Windows.DevHome.SDK.DevHomeContract, 1)] runtimeclass RepositoryUriSupportResult { RepositoryUriSupportResult(Boolean isSupported); diff --git a/extensionsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.vcxproj b/extensionsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.vcxproj index 315475c8fe..dabcd9af2e 100644 --- a/extensionsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.vcxproj +++ b/extensionsdk/Microsoft.Windows.DevHome.SDK/Microsoft.Windows.DevHome.SDK.vcxproj @@ -226,6 +226,7 @@ + @@ -260,6 +261,7 @@ + diff --git a/extensionsdk/Microsoft.Windows.DevHome.SDK/RepositoriesResult.cpp b/extensionsdk/Microsoft.Windows.DevHome.SDK/RepositoriesResult.cpp index 775d63a36c..309282d0f3 100644 --- a/extensionsdk/Microsoft.Windows.DevHome.SDK/RepositoriesResult.cpp +++ b/extensionsdk/Microsoft.Windows.DevHome.SDK/RepositoriesResult.cpp @@ -13,6 +13,7 @@ namespace winrt::Microsoft::Windows::DevHome::SDK::implementation RepositoriesResult::RepositoriesResult(winrt::hresult const& e, hstring const& diagnosticText) { _Result = std::make_shared(winrt::Microsoft::Windows::DevHome::SDK::ProviderOperationStatus::Failure, winrt::hresult(e), winrt::to_hstring("Something went wrong"), diagnosticText); + _Result = std::make_shared(winrt::Microsoft::Windows::DevHome::SDK::ProviderOperationStatus::Failure, winrt::hresult(e), diagnosticText, diagnosticText); } winrt::Windows::Foundation::Collections::IIterable RepositoriesResult::Repositories() diff --git a/extensionsdk/Microsoft.Windows.DevHome.SDK/RepositoriesSearchResult.cpp b/extensionsdk/Microsoft.Windows.DevHome.SDK/RepositoriesSearchResult.cpp new file mode 100644 index 0000000000..985141e4f0 --- /dev/null +++ b/extensionsdk/Microsoft.Windows.DevHome.SDK/RepositoriesSearchResult.cpp @@ -0,0 +1,58 @@ +#include "pch.h" +#include "RepositoriesSearchResult.h" +#include "RepositoriesSearchResult.g.cpp" + +namespace winrt::Microsoft::Windows::DevHome::SDK::implementation +{ + RepositoriesSearchResult::RepositoriesSearchResult(winrt::Windows::Foundation::Collections::IIterable const& repositories) : + _Repositories(std::make_shared>(repositories)), + _SelectionOptionsLabel(L""), + _SelectionOptionsName(L""), + _SelectionOptions(std::vector()), + _Result(winrt::Microsoft::Windows::DevHome::SDK::ProviderOperationResult(winrt::Microsoft::Windows::DevHome::SDK::ProviderOperationStatus::Success, winrt::hresult(S_OK), winrt::to_hstring(""), winrt::to_hstring(""))) + { + } + + RepositoriesSearchResult::RepositoriesSearchResult(winrt::Windows::Foundation::Collections::IIterable const& repositories, hstring const& selectionOptionsLabel, array_view selectionOptions, hstring const& selectionOptionsName) : + _Repositories(std::make_shared>(repositories)), + _SelectionOptionsLabel(selectionOptionsLabel), + _SelectionOptionsName(selectionOptionsName), + _SelectionOptions(std::vector{ selectionOptions.begin(), selectionOptions.end() }), + _Result(winrt::Microsoft::Windows::DevHome::SDK::ProviderOperationResult(winrt::Microsoft::Windows::DevHome::SDK::ProviderOperationStatus::Success, winrt::hresult(S_OK), winrt::to_hstring(""), winrt::to_hstring(""))) + { + } + + RepositoriesSearchResult::RepositoriesSearchResult(winrt::hresult const& e, hstring const& diagnosticText) : + _Repositories(std::make_shared>(winrt::Windows::Foundation::Collections::IIterable())), + _SelectionOptionsLabel(L""), + _SelectionOptionsName(L""), + _SelectionOptions(std::vector()), + _Result(winrt::Microsoft::Windows::DevHome::SDK::ProviderOperationResult(winrt::Microsoft::Windows::DevHome::SDK::ProviderOperationStatus::Failure, winrt::hresult(e), diagnosticText, diagnosticText)) + { + } + + winrt::Windows::Foundation::Collections::IIterable RepositoriesSearchResult::Repositories() + { + return *_Repositories.get(); + } + + hstring RepositoriesSearchResult::SelectionOptionsLabel() + { + return _SelectionOptionsLabel; + } + + com_array RepositoriesSearchResult::SelectionOptions() + { + return winrt::com_array{ _SelectionOptions }; + } + + hstring RepositoriesSearchResult::SelectionOptionsName() + { + return _SelectionOptionsName; + } + + winrt::Microsoft::Windows::DevHome::SDK::ProviderOperationResult RepositoriesSearchResult::Result() + { + return _Result; + } +} \ No newline at end of file diff --git a/extensionsdk/Microsoft.Windows.DevHome.SDK/RepositoriesSearchResult.h b/extensionsdk/Microsoft.Windows.DevHome.SDK/RepositoriesSearchResult.h new file mode 100644 index 0000000000..2a7c864dae --- /dev/null +++ b/extensionsdk/Microsoft.Windows.DevHome.SDK/RepositoriesSearchResult.h @@ -0,0 +1,32 @@ +#pragma once +#include "RepositoriesSearchResult.g.h" + +namespace winrt::Microsoft::Windows::DevHome::SDK::implementation +{ + struct RepositoriesSearchResult : RepositoriesSearchResultT + { + RepositoriesSearchResult() = default; + + RepositoriesSearchResult(winrt::Windows::Foundation::Collections::IIterable const& repositories); + RepositoriesSearchResult(winrt::Windows::Foundation::Collections::IIterable const& repositories, hstring const& searchPath, array_view selectionOptions, hstring const& selectionOptionsName); + RepositoriesSearchResult(winrt::hresult const& e, hstring const& diagnosticText); + winrt::Windows::Foundation::Collections::IIterable Repositories(); + hstring SelectionOptionsLabel(); + com_array SelectionOptions(); + hstring SelectionOptionsName(); + winrt::Microsoft::Windows::DevHome::SDK::ProviderOperationResult Result(); + + private: + winrt::Microsoft::Windows::DevHome::SDK::ProviderOperationResult _Result; + std::shared_ptr> _Repositories; + hstring _SelectionOptionsLabel; + std::vector _SelectionOptions; + hstring _SelectionOptionsName; + }; +} +namespace winrt::Microsoft::Windows::DevHome::SDK::factory_implementation +{ + struct RepositoriesSearchResult : RepositoriesSearchResultT + { + }; +} \ No newline at end of file From f3b2bbfed01c3673d6bfb8a8aabb1c4adce8c0f6 Mon Sep 17 00:00:00 2001 From: sshilov7 <51001703+sshilov7@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:28:42 -0700 Subject: [PATCH 010/121] Add DevSetupEngine COM permissions and Dispose method to force shutdown. (#2358) --- .../src/DevSetupAgent/Requests/ConfigureRequest.cs | 9 +++++++-- HyperVExtension/src/DevSetupEngine/Program.cs | 14 +++++++++++++- .../Microsoft.Windows.DevHome.DevSetupEngine.idl | 1 + 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/HyperVExtension/src/DevSetupAgent/Requests/ConfigureRequest.cs b/HyperVExtension/src/DevSetupAgent/Requests/ConfigureRequest.cs index 8f8b699055..57f27437a4 100644 --- a/HyperVExtension/src/DevSetupAgent/Requests/ConfigureRequest.cs +++ b/HyperVExtension/src/DevSetupAgent/Requests/ConfigureRequest.cs @@ -32,8 +32,8 @@ public ConfigureRequest(IRequestContext requestContext) public override IHostResponse Execute(ProgressHandler progressHandler, CancellationToken stoppingToken) { - // DevSetupEngine needs to be started manually from command line in the test. var devSetupEnginePtr = IntPtr.Zero; + var devSetupEngine = default(IDevSetupEngine); try { var hr = PInvoke.CoCreateInstance(Guid.Parse("82E86C64-A8B9-44F9-9323-C37982F2D8BE"), null, CLSCTX.CLSCTX_LOCAL_SERVER, typeof(IDevSetupEngine).GUID, out var devSetupEngineObj); @@ -44,7 +44,7 @@ public override IHostResponse Execute(ProgressHandler progressHandler, Cancellat devSetupEnginePtr = Marshal.GetIUnknownForObject(devSetupEngineObj); - var devSetupEngine = MarshalInterface.FromAbi(devSetupEnginePtr); + devSetupEngine = MarshalInterface.FromAbi(devSetupEnginePtr); var operation = devSetupEngine.ApplyConfigurationAsync(ConfigureData); uint progressCounter = 0; @@ -66,6 +66,11 @@ public override IHostResponse Execute(ProgressHandler progressHandler, Cancellat { Marshal.Release(devSetupEnginePtr); } + + if (devSetupEngine != null) + { + devSetupEngine.Dispose(); + } } } } diff --git a/HyperVExtension/src/DevSetupEngine/Program.cs b/HyperVExtension/src/DevSetupEngine/Program.cs index 44816e9348..1b1bd2ac4b 100644 --- a/HyperVExtension/src/DevSetupEngine/Program.cs +++ b/HyperVExtension/src/DevSetupEngine/Program.cs @@ -2,7 +2,7 @@ // Licensed under the MIT License. using System.ComponentModel; -using HyperVExtension.DevSetupEngine; +using System.Security.AccessControl; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Win32; @@ -81,6 +81,18 @@ private static void RegisterComServer() var appIdKey = Registry.LocalMachine.CreateSubKey(AppIdPath + appId, true) ?? throw new Win32Exception(); appIdKey.SetValue("RunAs", "Interactive User", RegistryValueKind.String); + // O:PSG: BU Owner: principal self, Group: Built-in users + // (A; ; 0xB; ; ; SY) Allow SYSTEM + // (A; ; 0xB; ; ; LS) Allow Local Service + // (A; ; 0xB; ; ; PS) Allow Principal self + // 0xB = (COM_RIGHTS_EXECUTE | COM_RIGHTS_EXECUTE_LOCAL | COM_RIGHTS_ACTIVATE_LOCAL + var permissions = "O:PSG:BUD:(A;;0xB;;;SY)(A;;0xB;;;LS)(A;;0xB;;;PS)"; + RawSecurityDescriptor rawSd = new RawSecurityDescriptor(permissions); + var sdBinaryForm = new byte[rawSd.BinaryLength]; + rawSd.GetBinaryForm(sdBinaryForm, 0); + appIdKey.SetValue("AccessPermission", sdBinaryForm, RegistryValueKind.Binary); + appIdKey.SetValue("LaunchPermission", sdBinaryForm, RegistryValueKind.Binary); + var clsIdKey = Registry.LocalMachine.CreateSubKey(ClsIdIdPath + appId, true) ?? throw new Win32Exception(); clsIdKey.SetValue("AppID", appId); diff --git a/HyperVExtension/src/DevSetupEngineIdl/Microsoft.Windows.DevHome.DevSetupEngine.idl b/HyperVExtension/src/DevSetupEngineIdl/Microsoft.Windows.DevHome.DevSetupEngine.idl index a2dc1707b7..de0ea82f9a 100644 --- a/HyperVExtension/src/DevSetupEngineIdl/Microsoft.Windows.DevHome.DevSetupEngine.idl +++ b/HyperVExtension/src/DevSetupEngineIdl/Microsoft.Windows.DevHome.DevSetupEngine.idl @@ -217,5 +217,6 @@ namespace Microsoft.Windows.DevHome.DevSetupEngine { // Applies the configuration set state. Windows.Foundation.IAsyncOperationWithProgress ApplyConfigurationAsync(String content); + void Dispose(); }; } \ No newline at end of file From 4c7381c6b48cca55ecc70a2f1c1502dff560c8d9 Mon Sep 17 00:00:00 2001 From: Gobinath B Date: Wed, 20 Mar 2024 03:21:34 +0530 Subject: [PATCH 011/121] Add Usage Percentage to Memory widget (#2335) --- .../Templates/SystemMemoryTemplate.json | 327 ++++++++++-------- 1 file changed, 175 insertions(+), 152 deletions(-) diff --git a/extensions/CoreWidgetProvider/Widgets/Templates/SystemMemoryTemplate.json b/extensions/CoreWidgetProvider/Widgets/Templates/SystemMemoryTemplate.json index 8ae5b289b9..eaf2ce046c 100644 --- a/extensions/CoreWidgetProvider/Widgets/Templates/SystemMemoryTemplate.json +++ b/extensions/CoreWidgetProvider/Widgets/Templates/SystemMemoryTemplate.json @@ -1,152 +1,175 @@ -{ - "type": "AdaptiveCard", - "body": [ - { - "type": "Container", - "$when": "${errorMessage != null}", - "items": [ - { - "type": "TextBlock", - "text": "${errorMessage}", - "wrap": true, - "size": "small" - } - ], - "style": "warning" - }, - { - "type": "Container", - "$when": "${errorMessage == null}", - "items": [ - { - "type": "Image", - "url": "${memGraphUrl}", - "height": "${chartHeight}", - "width": "${chartWidth}", - "horizontalAlignment": "center" - }, - { - "type": "ColumnSet", - "columns": [ - { - "type": "Column", - "items": [ - { - "text": "%Memory_Widget_Template/UsedMemory%", - "type": "TextBlock", - "size": "small", - "isSubtle": true - }, - { - "text": "${usedMem}", - "type": "TextBlock", - "size": "large", - "weight": "bolder" - } - ] - }, - { - "type": "Column", - "items": [ - { - "text": "%Memory_Widget_Template/AllMemory%", - "type": "TextBlock", - "size": "small", - "isSubtle": true, - "horizontalAlignment": "right" - }, - { - "text": "${allMem}", - "type": "TextBlock", - "size": "large", - "weight": "bolder", - "horizontalAlignment": "right" - } - ] - } - ] - }, - { - "type": "ColumnSet", - "columns": [ - { - "type": "Column", - "items": [ - { - "text": "%Memory_Widget_Template/Committed%", - "type": "TextBlock", - "size": "small", - "isSubtle": true - }, - { - "text": "${committedMem}/${committedLimitMem}", - "type": "TextBlock", - "size": "medium" - } - ] - }, - { - "type": "Column", - "items": [ - { - "text": "%Memory_Widget_Template/Cached%", - "type": "TextBlock", - "size": "small", - "isSubtle": true, - "horizontalAlignment": "right" - }, - { - "text": "${cachedMem}", - "type": "TextBlock", - "size": "medium", - "horizontalAlignment": "right" - } - ] - } - ] - }, - { - "type": "ColumnSet", - "columns": [ - { - "type": "Column", - "items": [ - { - "text": "%Memory_Widget_Template/PagedPool%", - "type": "TextBlock", - "size": "small", - "isSubtle": true - }, - { - "text": "${pagedPoolMem}", - "type": "TextBlock", - "size": "medium" - } - ] - }, - { - "type": "Column", - "items": [ - { - "text": "%Memory_Widget_Template/NonPagedPool%", - "type": "TextBlock", - "size": "small", - "isSubtle": true, - "horizontalAlignment": "right" - }, - { - "text": "${nonPagedPoolMem}", - "type": "TextBlock", - "size": "medium", - "horizontalAlignment": "right" - } - ] - } - ] - } - ] - } - ], - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "version": "1.5" -} \ No newline at end of file +{ + "type": "AdaptiveCard", + "body": [ + { + "type": "Container", + "$when": "${errorMessage != null}", + "items": [ + { + "type": "TextBlock", + "text": "${errorMessage}", + "wrap": true, + "size": "small" + } + ], + "style": "warning" + }, + { + "type": "Container", + "$when": "${errorMessage == null}", + "items": [ + { + "type": "Image", + "url": "${memGraphUrl}", + "height": "${chartHeight}", + "width": "${chartWidth}", + "horizontalAlignment": "center" + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "items": [ + { + "text": "%Memory_Widget_Template/UsedMemory%", + "type": "TextBlock", + "size": "small", + "isSubtle": true + }, + { + "text": "${usedMem}", + "type": "TextBlock", + "size": "large", + "weight": "bolder" + } + ] + }, + { + "type": "Column", + "items": [ + { + "text": "%Memory_Widget_Template/AllMemory%", + "type": "TextBlock", + "size": "small", + "isSubtle": true, + "horizontalAlignment": "right" + }, + { + "text": "${allMem}", + "type": "TextBlock", + "size": "large", + "weight": "bolder", + "horizontalAlignment": "right" + } + ] + } + ] + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "items": [ + { + "text": "%Memory_Widget_Template/Committed%", + "type": "TextBlock", + "size": "small", + "isSubtle": true + }, + { + "text": "${committedMem}/${committedLimitMem}", + "type": "TextBlock", + "size": "medium" + } + ] + }, + { + "type": "Column", + "items": [ + { + "text": "%Memory_Widget_Template/Cached%", + "type": "TextBlock", + "size": "small", + "isSubtle": true, + "horizontalAlignment": "right" + }, + { + "text": "${cachedMem}", + "type": "TextBlock", + "size": "medium", + "horizontalAlignment": "right" + } + ] + } + ] + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "items": [ + { + "text": "%Memory_Widget_Template/PagedPool%", + "type": "TextBlock", + "size": "small", + "isSubtle": true + }, + { + "text": "${pagedPoolMem}", + "type": "TextBlock", + "size": "medium" + } + ] + }, + { + "type": "Column", + "items": [ + { + "text": "%Memory_Widget_Template/NonPagedPool%", + "type": "TextBlock", + "size": "small", + "isSubtle": true, + "horizontalAlignment": "right" + }, + { + "text": "${nonPagedPoolMem}", + "type": "TextBlock", + "size": "medium", + "horizontalAlignment": "right" + } + ] + } + ] + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "items": [ + { + "text": "%Memory_Widget_Template/MemoryUsage%", + "type": "TextBlock", + "size": "small", + "isSubtle": true, + "horizontalAlignment": "right" + }, + { + "text": "${memUsage}", + "type": "TextBlock", + "size": "medium", + "horizontalAlignment": "right" + } + ] + } + ] + } + ] + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.5" +} From 42c54a044fd3c89c7732ff66a0123ae56c39984d Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:54:02 -0400 Subject: [PATCH 012/121] Fix scroll regions across Dev Home (#2423) --- .../Styles/HorizontalCardStyles.xaml | 7 +- .../Templates/EnvironmentsTemplates.xaml | 1 - .../DevHome.Settings/Views/AboutPage.xaml | 11 +- .../DevHome.Settings/Views/AccountsPage.xaml | 10 +- .../Views/ExperimentalFeaturesPage.xaml | 10 +- .../DevHome.Settings/Views/FeedbackPage.xaml | 12 +- .../Views/PreferencesPage.xaml | 18 +- .../DevHome.Settings/Views/SettingsPage.xaml | 10 +- src/Styles/Thickness.xaml | 2 + src/Views/WhatsNewPage.xaml | 11 +- .../Views/DashboardView.xaml | 9 +- .../Views/LandingPage.xaml | 56 +- .../Views/ExtensionLibraryView.xaml | 4 +- .../Views/ExtensionSettingsPage.xaml | 9 +- .../Controls/SetupShell.xaml | 20 +- .../Controls/SetupShell.xaml.cs | 2 - .../Views/AppManagementView.xaml | 2 +- .../DevHome.SetupFlow/Views/LoadingView.xaml | 2 +- .../DevHome.SetupFlow/Views/MainPageView.xaml | 4 +- .../Views/RepoConfigView.xaml | 2 +- .../Views/SetupFlowPage.xaml | 9 +- .../Views/SetupTargetView.xaml | 4 +- .../DevHome.SetupFlow/Views/SummaryView.xaml | 650 +++++++++--------- 23 files changed, 432 insertions(+), 433 deletions(-) diff --git a/common/Environments/Styles/HorizontalCardStyles.xaml b/common/Environments/Styles/HorizontalCardStyles.xaml index 7b9123729e..a6b2e98f49 100644 --- a/common/Environments/Styles/HorizontalCardStyles.xaml +++ b/common/Environments/Styles/HorizontalCardStyles.xaml @@ -15,6 +15,7 @@ + @@ -33,7 +34,6 @@ BasedOn="{StaticResource DefaultListViewItemStyle}"> - @@ -42,7 +42,9 @@ x:Key="HorizontalCardListViewItemContainerForManagementPageStyle" TargetType="ListViewItem" BasedOn="{StaticResource DefaultListViewItemStyle}"> - + + + @@ -50,7 +52,6 @@ diff --git a/common/Environments/Templates/EnvironmentsTemplates.xaml b/common/Environments/Templates/EnvironmentsTemplates.xaml index 4538248f20..ccbc834b19 100644 --- a/common/Environments/Templates/EnvironmentsTemplates.xaml +++ b/common/Environments/Templates/EnvironmentsTemplates.xaml @@ -4,7 +4,6 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:labs="using:CommunityToolkit.Labs.WinUI" - xmlns:models="using:DevHome.Common.Environments.Models" xmlns:converters="using:CommunityToolkit.WinUI.Converters"> + 40,0,40,22 36,24,36,0 diff --git a/src/Views/WhatsNewPage.xaml b/src/Views/WhatsNewPage.xaml index 19da4938a4..256df8de94 100644 --- a/src/Views/WhatsNewPage.xaml +++ b/src/Views/WhatsNewPage.xaml @@ -9,7 +9,6 @@ xmlns:models="using:DevHome.Models" xmlns:behaviors="using:DevHome.Common.Behaviors" xmlns:controls="using:CommunityToolkit.WinUI.Controls" - xmlns:converters="using:CommunityToolkit.WinUI.Converters" behaviors:NavigationViewHeaderBehavior.HeaderMode="Never" Loaded="OnLoaded" SizeChanged="OnSizeChanged" @@ -34,10 +33,14 @@ - - + + - + diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml index 0c02bf3f56..22512232aa 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml @@ -44,16 +44,17 @@ - + - + - + - + + - + - + @@ -163,7 +162,8 @@ - diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml index 7c9d90bb48..20edc373a4 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml @@ -38,9 +38,9 @@ - + - + - + - + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml b/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml index 663c0f06df..17ca1db3f8 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml @@ -20,7 +20,8 @@ - + - + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml.cs index 59efa6b50a..6a5cc42e19 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml.cs @@ -1,11 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using DevHome.SetupFlow.Services; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Markup; namespace DevHome.SetupFlow.Controls; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml index c302b0051f..a87cb0358b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml @@ -31,7 +31,7 @@ - + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/LoadingView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/LoadingView.xaml index f1e54d2fc4..d1f862c464 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/LoadingView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/LoadingView.xaml @@ -52,7 +52,7 @@ - + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml index e1f5ad7101..f484544cf6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml @@ -75,7 +75,7 @@ - + - + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml index ff6ffb1a6a..bc2c11c908 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml @@ -32,7 +32,7 @@ - diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml index 3eb68014a7..79e5e95a25 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml @@ -4,9 +4,7 @@ + behaviors:NavigationViewHeaderBehavior.HeaderMode="Never"> - + @@ -83,6 +79,7 @@ diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupTargetView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupTargetView.xaml index 2e2137a37c..fd65447757 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupTargetView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupTargetView.xaml @@ -231,8 +231,8 @@ - - + + - - - - - - - + + + + + + + - - - - - - - - - - - - - + + + + From 3e77990ceaa28e78cdd9f9b06438a3b26988b42b Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:54:55 -0400 Subject: [PATCH 071/121] Use Package and Extension display names (#2544) --- common/Services/ComputeSystemService.cs | 2 +- common/Services/IExtensionWrapper.cs | 47 ++++++------------- .../Views/FeedbackPage.xaml.cs | 2 +- src/Models/ExtensionWrapper.cs | 45 +++++------------- src/Services/AccountsService.cs | 2 +- .../TestModels/TestExtensionWrapper.cs | 4 +- .../ViewModels/ExtensionLibraryViewModel.cs | 6 +-- .../ViewModels/ExtensionSettingsViewModel.cs | 2 +- .../ViewModels/InstalledPackageViewModel.cs | 6 +-- .../Views/ExtensionLibraryView.xaml | 4 +- .../Models/RepositoryProvider.cs | 2 +- .../Models/RepositoryProviders.cs | 2 +- .../WinGetFeaturedApplicationsDataSource.cs | 2 +- .../ViewModels/AddRepoViewModel.cs | 2 +- 14 files changed, 45 insertions(+), 83 deletions(-) diff --git a/common/Services/ComputeSystemService.cs b/common/Services/ComputeSystemService.cs index dd24c44cc9..42be48d4b5 100644 --- a/common/Services/ComputeSystemService.cs +++ b/common/Services/ComputeSystemService.cs @@ -83,7 +83,7 @@ public async Task> GetComputeSystemProvidersA } catch (Exception ex) { - Log.Error($"Failed to get {nameof(IComputeSystemProvider)} provider from '{extension.Name}'", ex); + Log.Error($"Failed to get {nameof(IComputeSystemProvider)} provider from '{extension.PackageFamilyName}/{extension.ExtensionDisplayName}'", ex); } } diff --git a/common/Services/IExtensionWrapper.cs b/common/Services/IExtensionWrapper.cs index 026d36c1dc..7a861502c9 100644 --- a/common/Services/IExtensionWrapper.cs +++ b/common/Services/IExtensionWrapper.cs @@ -12,68 +12,49 @@ namespace DevHome.Common.Services; public interface IExtensionWrapper { /// - /// Gets name of the extension as mentioned in the manifest + /// Gets the DisplayName of the package as mentioned in the manifest /// - string Name - { - get; - } + string PackageDisplayName { get; } + + /// + /// Gets DisplayName of the extension as mentioned in the manifest + /// + string ExtensionDisplayName { get; } /// /// Gets PackageFullName of the extension /// - string PackageFullName - { - get; - } + string PackageFullName { get; } /// /// Gets PackageFamilyName of the extension /// - string PackageFamilyName - { - get; - } + string PackageFamilyName { get; } /// /// Gets Publisher of the extension /// - string Publisher - { - get; - } + string Publisher { get; } /// /// Gets class id (GUID) of the extension class (which implements IExtension) as mentioned in the manifest /// - string ExtensionClassId - { - get; - } + string ExtensionClassId { get; } /// /// Gets the date on which the application package was installed or last updated. /// - DateTimeOffset InstalledDate - { - get; - } + DateTimeOffset InstalledDate { get; } /// /// Gets the PackageVersion of the extension /// - PackageVersion Version - { - get; - } + PackageVersion Version { get; } /// /// Gets the Unique Id for the extension /// - public string ExtensionUniqueId - { - get; - } + public string ExtensionUniqueId { get; } /// /// Checks whether we have a reference to the extension process and we are able to call methods on the interface. diff --git a/settings/DevHome.Settings/Views/FeedbackPage.xaml.cs b/settings/DevHome.Settings/Views/FeedbackPage.xaml.cs index 5cb33dbf36..90f6f3607f 100644 --- a/settings/DevHome.Settings/Views/FeedbackPage.xaml.cs +++ b/settings/DevHome.Settings/Views/FeedbackPage.xaml.cs @@ -297,7 +297,7 @@ private string GetExtensions() var extensionsStr = stringResource.GetLocalized("Settings_Feedback_Extensions") + ": \n"; foreach (var extension in extensions) { - extensionsStr += extension.PackageFullName + "\n"; + extensionsStr += extension.PackageFullName + " (" + extension.ExtensionDisplayName + ")\n"; } return extensionsStr; diff --git a/src/Models/ExtensionWrapper.cs b/src/Models/ExtensionWrapper.cs index 2b6552c633..7f5743aff4 100644 --- a/src/Models/ExtensionWrapper.cs +++ b/src/Models/ExtensionWrapper.cs @@ -32,7 +32,8 @@ public class ExtensionWrapper : IExtensionWrapper public ExtensionWrapper(AppExtension appExtension, string classId) { - Name = appExtension.DisplayName; + PackageDisplayName = appExtension.Package.DisplayName; + ExtensionDisplayName = appExtension.DisplayName; PackageFullName = appExtension.Package.Id.FullName; PackageFamilyName = appExtension.Package.Id.FamilyName; ExtensionClassId = classId ?? throw new ArgumentNullException(nameof(classId)); @@ -42,40 +43,21 @@ public ExtensionWrapper(AppExtension appExtension, string classId) ExtensionUniqueId = appExtension.AppInfo.AppUserModelId + "!" + appExtension.Id; } - public string Name - { - get; - } + public string PackageDisplayName { get; } - public string PackageFullName - { - get; - } + public string ExtensionDisplayName { get; } - public string PackageFamilyName - { - get; - } + public string PackageFullName { get; } - public string ExtensionClassId - { - get; - } + public string PackageFamilyName { get; } - public string Publisher - { - get; - } + public string ExtensionClassId { get; } - public DateTimeOffset InstalledDate - { - get; - } + public string Publisher { get; } - public PackageVersion Version - { - get; - } + public DateTimeOffset InstalledDate { get; } + + public PackageVersion Version { get; } /// /// Gets the unique id for this Dev Home extension. The unique id is a concatenation of: @@ -86,10 +68,7 @@ public PackageVersion Version /// The Extension Id. This is the unique identifier of the extension within the application. /// /// - public string ExtensionUniqueId - { - get; - } + public string ExtensionUniqueId { get; } public bool IsRunning() { diff --git a/src/Services/AccountsService.cs b/src/Services/AccountsService.cs index 7127352bf4..0c628f7a5e 100644 --- a/src/Services/AccountsService.cs +++ b/src/Services/AccountsService.cs @@ -59,7 +59,7 @@ public async Task> GetDevIdProviders() } catch (Exception ex) { - _log.Error($"Failed to get {nameof(IDeveloperIdProvider)} provider from '{extension.Name}'", ex); + _log.Error($"Failed to get {nameof(IDeveloperIdProvider)} provider from '{extension.PackageFamilyName}/{extension.ExtensionDisplayName}'", ex); } } diff --git a/tools/Environments/DevHome.Environments/TestModels/TestExtensionWrapper.cs b/tools/Environments/DevHome.Environments/TestModels/TestExtensionWrapper.cs index aba63a03af..4a066c0e05 100644 --- a/tools/Environments/DevHome.Environments/TestModels/TestExtensionWrapper.cs +++ b/tools/Environments/DevHome.Environments/TestModels/TestExtensionWrapper.cs @@ -12,7 +12,9 @@ namespace DevHome.Environments.TestModels; public class TestExtensionWrapper : IExtensionWrapper { - public string Name => throw new NotImplementedException(); + public string PackageDisplayName => throw new NotImplementedException(); + + public string ExtensionDisplayName => throw new NotImplementedException(); public string PackageFullName => "Microsoft.Windows.DevHome.Dev_0.0.0.0_x64__8wekyb3d8bbwe"; diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs index 53b24f8638..01f434838a 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs @@ -79,7 +79,7 @@ private async Task GetInstalledExtensionsAsync() InstalledPackagesList.Clear(); - extensionWrappers = extensionWrappers.OrderBy(extensionWrapper => extensionWrapper.Name); + extensionWrappers = extensionWrappers.OrderBy(extensionWrapper => extensionWrapper.PackageDisplayName); foreach (var extensionWrapper in extensionWrappers) { @@ -90,7 +90,7 @@ private async Task GetInstalledExtensionsAsync() } var hasSettingsProvider = extensionWrapper.HasProviderType(ProviderType.Settings); - var extension = new InstalledExtensionViewModel(extensionWrapper.Name, extensionWrapper.ExtensionUniqueId, hasSettingsProvider); + var extension = new InstalledExtensionViewModel(extensionWrapper.ExtensionDisplayName, extensionWrapper.ExtensionUniqueId, hasSettingsProvider); // Each extension is shown under the package that contains it. Check if we have the package in the list // already and if not, create it and add it to the list of packages. Then add the extension to that @@ -99,7 +99,7 @@ private async Task GetInstalledExtensionsAsync() if (package == null) { package = new InstalledPackageViewModel( - extensionWrapper.Name, + extensionWrapper.PackageDisplayName, extensionWrapper.Publisher, extensionWrapper.PackageFamilyName, extensionWrapper.InstalledDate, diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs index cb1aac5ad3..3a4cb47105 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs @@ -46,7 +46,7 @@ private async Task OnSettingsContentLoadedAsync(ExtensionAdaptiveCardPanel exten if ((_navigationService.LastParameterUsed != null) && ((string)_navigationService.LastParameterUsed == extensionWrapper.ExtensionUniqueId)) { - FillBreadcrumbBar(extensionWrapper.Name); + FillBreadcrumbBar(extensionWrapper.ExtensionDisplayName); var settingsProvider = Task.Run(() => extensionWrapper.GetProviderAsync()).Result; if (settingsProvider != null) diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/InstalledPackageViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/InstalledPackageViewModel.cs index 3232666dad..7593a2ac7a 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/InstalledPackageViewModel.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/InstalledPackageViewModel.cs @@ -87,7 +87,7 @@ private void NavigateSettings() public partial class InstalledPackageViewModel : ObservableObject { [ObservableProperty] - private string _title; + private string _displayName; [ObservableProperty] private string _publisher; @@ -103,9 +103,9 @@ public partial class InstalledPackageViewModel : ObservableObject public ObservableCollection InstalledExtensionsList { get; set; } - public InstalledPackageViewModel(string title, string publisher, string packageFamilyName, DateTimeOffset installedDate, PackageVersion version) + public InstalledPackageViewModel(string displayName, string publisher, string packageFamilyName, DateTimeOffset installedDate, PackageVersion version) { - _title = title; + _displayName = displayName; _publisher = publisher; _packageFamilyName = packageFamilyName; _installedDate = installedDate; diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml index 60cdaf1775..5f56915dde 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml @@ -62,7 +62,7 @@ - - diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs index 11610776ae..dc47ac7579 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs @@ -60,7 +60,7 @@ public RepositoryProvider(IExtensionWrapper extensionWrapper) public string DisplayName => _repositoryProvider.DisplayName; - public string ExtensionDisplayName => _extensionWrapper.Name; + public string ExtensionDisplayName => _extensionWrapper.ExtensionDisplayName; /// /// Starts the extension if it isn't running. diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProviders.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProviders.cs index 7fcdb85427..346ddb20ea 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProviders.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProviders.cs @@ -37,7 +37,7 @@ public string DisplayName(string providerName) public RepositoryProviders(IEnumerable extensionWrappers) { - _providers = extensionWrappers.ToDictionary(extensionWrapper => extensionWrapper.Name, extensionWrapper => new RepositoryProvider(extensionWrapper)); + _providers = extensionWrappers.ToDictionary(extensionWrapper => extensionWrapper.ExtensionDisplayName, extensionWrapper => new RepositoryProvider(extensionWrapper)); } public void StartAllExtensions() diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs index b58908efbc..a4c8819ced 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs @@ -171,7 +171,7 @@ private async Task ForEachEnabledExtensionAsync(Func extension.HasProviderType(ProviderType.Repository) && - extension.HasProviderType(ProviderType.DeveloperId)).OrderBy(extensionWrapper => extensionWrapper.Name); + extension.HasProviderType(ProviderType.DeveloperId)).OrderBy(extensionWrapper => extensionWrapper.ExtensionDisplayName); _providers = new RepositoryProviders(extensions); From bbd93a0d7c156d8d3476731c821f62f5ca669fb8 Mon Sep 17 00:00:00 2001 From: Felipe G Date: Mon, 8 Apr 2024 10:41:28 -0700 Subject: [PATCH 072/121] Announcing removing widgets from dashboard (#2537) * adding call to screen reader service * Adding string to be said * moving logic to control code --------- Co-authored-by: Felipe da Conceicao Guimaraes --- src/Views/ShellPage.xaml | 2 +- .../Controls/WidgetControl.xaml.cs | 2 + .../Strings/en-us/Resources.resw | 65 ++++++++++++++++++- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/Views/ShellPage.xaml b/src/Views/ShellPage.xaml index 37b0b4086f..2b58a85590 100644 --- a/src/Views/ShellPage.xaml +++ b/src/Views/ShellPage.xaml @@ -126,7 +126,7 @@ from reading the control value. --> - + diff --git a/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs index d3b607beaa..c559a4ee09 100644 --- a/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs @@ -105,6 +105,8 @@ private async void OnRemoveWidgetClick(object sender, RoutedEventArgs e) var widgetIdToDelete = widgetViewModel.Widget.Id; var widgetToDelete = widgetViewModel.Widget; _log.Debug($"User removed widget, delete widget {widgetIdToDelete}"); + var stringResource = new StringResource("DevHome.Dashboard.pri", "DevHome.Dashboard/Resources"); + Application.Current.GetService().Announce(stringResource.GetLocalized("WidgetRemoved")); DashboardView.PinnedWidgets.Remove(widgetViewModel); try { diff --git a/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw b/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw index b1231169a4..077df6bc2e 100644 --- a/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw +++ b/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw @@ -1,5 +1,64 @@  + @@ -190,4 +249,8 @@ Close Text for a button to close a content dialog. - + + Widget removed + This is said by narrator whenever a widget is removed + + \ No newline at end of file From efdffbc5d8c6c90854edb594609da9da69b4ac90 Mon Sep 17 00:00:00 2001 From: Felipe G Date: Mon, 8 Apr 2024 11:13:27 -0700 Subject: [PATCH 073/121] Adding AutomationProperties.Name string to dashboard page (#2545) * Adding string to dashboard page * Removing automation --------- Co-authored-by: Felipe da Conceicao Guimaraes --- .../DevHome.Dashboard/Strings/en-us/Resources.resw | 6 +++++- tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw b/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw index 077df6bc2e..5ca6692709 100644 --- a/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw +++ b/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw @@ -249,8 +249,12 @@ Close Text for a button to close a content dialog. + + Dashboard + Dashboard accessible name to be narrated + Widget removed This is said by narrator whenever a widget is removed - \ No newline at end of file + diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml index 22512232aa..c2f82f92e8 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml @@ -3,6 +3,7 @@ Date: Tue, 9 Apr 2024 09:47:10 -0700 Subject: [PATCH 074/121] Finish UI for environments creation (#2539) * add initial changes * add parsers * fix build * update existing classes and xaml * add hyper-v adaptive card for creation * add changes for hyper-v * fix build * add changes to show settings cards from hyper v * update with latest changes * add recent changes * fix build * add updates for adaptive cards to appear in review page * add creation to experiements page and fix adaptive card not showing up after moving to a different page * add missing message classes * Delete tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs * update message passing * add initial changes * add more updates * update based on comments, and add more comments * update comments and itemsviews choice set to prevent the choiceset from holding onto items view control internally * Use adaptive card render service to render adaptive card within content dialog * add updates * add * update json * add more updates * add more changes * only update UI if status method returns a mail * add updates for UI * add finishing UI changed * update based on comments and make sure a new create environment flow is made when you click the create environment button * update spelling --- .../Extensions/StreamExtensions.cs | 15 +- .../Models/ByteTransferProgress.cs | 28 +++ .../VMGalleryCreationAdaptiveCardSession.cs | 32 +-- .../ArchiveExtractionReport.cs | 13 +- .../DotNetZipArchiveProvider.cs | 6 +- .../DownloadOperationReport.cs | 9 +- .../IOperationReport.cs | 4 +- .../VMGalleryCreationUserInput.cs | 2 +- .../VMGalleryVMCreationOperation.cs | 41 ++-- .../Services/DownloaderService.cs | 8 +- .../Strings/en-US/Resources.resw | 12 +- .../InitialVMGalleryCreationForm.json | 2 +- .../HyperVExtensionIntegrationTest.cs | 2 +- .../Services/HyperVProviderTests.cs | 2 +- .../Mocks/DownloaderServiceMock.cs | 9 +- .../CardStateColorToBrushConverter.cs | 1 + .../Environments/CustomControls/CardBody.xaml | 46 +++-- .../CustomControls/CardBody.xaml.cs | 16 ++ ...teCreateComputeSystemOperationException.cs | 18 ++ common/Environments/Models/CardProperty.cs | 1 + .../Models/ComputeSystemProvider.cs | 21 +- .../Models/CreateComputeSystemOperation.cs | 183 +++++++++++++++++ .../FailedCreateComputeSystemOperation.cs | 35 ++++ .../Services/ComputeSystemManager.cs | 45 ++++ .../Services/IComputeSystemManager.cs | 9 + common/Services/INavigationService.cs | 2 + common/Strings/en-us/Resources.resw | 12 +- .../DevHome.Environments.csproj | 1 - .../Selectors/CardItemTemplateSelector.cs | 42 ++++ .../Strings/en-us/Resources.resw | 28 +++ .../ViewModels/ComputeSystemCardBase.cs | 48 +++++ .../ViewModels/ComputeSystemViewModel.cs | 40 +--- .../CreateComputeSystemOperationViewModel.cs | 150 ++++++++++++++ .../ViewModels/LandingPageViewModel.cs | 129 +++++++++--- .../ViewModels/OperationsViewModel.cs | 32 ++- .../Views/LandingPage.xaml | 135 ++++++++---- .../Views/LandingPage.xaml.cs | 5 - .../Controls/SetupShell.xaml | 2 +- .../Controls/SetupShell.xaml.cs | 7 + .../Environments/CreateEnvironmentTask.cs | 193 ++++++++++++++++++ .../Services/SetupFlowOrchestrator.cs | 1 + .../Services/StringResourceKey.cs | 18 ++ .../Strings/en-us/Resources.resw | 63 +++++- .../EnvironmentCreationOptionsTaskGroup.cs | 5 +- .../CreateEnvironmentReviewViewModel.cs | 2 +- .../EnvironmentCreationOptionsViewModel.cs | 4 +- .../SelectEnvironmentProviderViewModel.cs | 1 - .../ViewModels/MainPageViewModel.cs | 4 +- .../ViewModels/ReviewViewModel.cs | 22 ++ .../ViewModels/SetupFlowViewModel.cs | 24 +++ .../ViewModels/SummaryViewModel.cs | 52 ++++- .../EnvironmentCreationOptionsView.xaml | 8 +- .../SelectEnvironmentProviderView.xaml | 4 +- .../DevHome.SetupFlow/Views/ReviewView.xaml | 13 +- .../Views/SetupFlowPage.xaml.cs | 7 + .../DevHome.SetupFlow/Views/SummaryView.xaml | 43 +++- .../Views/SummaryView.xaml.cs | 55 ++++- 57 files changed, 1485 insertions(+), 227 deletions(-) create mode 100644 HyperVExtension/src/HyperVExtension/Models/ByteTransferProgress.cs create mode 100644 common/Environments/Exceptions/CreateCreateComputeSystemOperationException.cs create mode 100644 common/Environments/Models/CreateComputeSystemOperation.cs create mode 100644 common/Environments/Models/FailedCreateComputeSystemOperation.cs create mode 100644 tools/Environments/DevHome.Environments/Selectors/CardItemTemplateSelector.cs create mode 100644 tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs create mode 100644 tools/Environments/DevHome.Environments/ViewModels/CreateComputeSystemOperationViewModel.cs create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs diff --git a/HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs b/HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs index a1af58afa9..c44703db17 100644 --- a/HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs +++ b/HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using HyperVExtension.Models; namespace HyperVExtension.Extensions; @@ -19,10 +20,11 @@ public static class StreamExtensions /// The object that progress will be reported to /// The size of the buffer which is used to read data from the source stream and write it to the destination stream /// A cancellation token that will allow the caller to cancel the operation - public static async Task CopyToAsync(this Stream source, Stream destination, IProgress progressProvider, int bufferSize, CancellationToken cancellationToken) + public static async Task CopyToAsync(this Stream source, Stream destination, IProgress progressProvider, int bufferSize, long totalBytesToExtract, CancellationToken cancellationToken) { var buffer = new byte[bufferSize]; long totalRead = 0; + var lastPercentage = 0U; while (true) { @@ -37,8 +39,15 @@ public static async Task CopyToAsync(this Stream source, Stream destination, IPr await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); totalRead += bytesRead; - // Report the progress of the operation. - progressProvider.Report(totalRead); + var progressPercentage = (uint)(totalRead / (double)totalBytesToExtract * 100D); + + // Only update progress when a whole percentage has been completed. + if (progressPercentage != lastPercentage) + { + // Report the progress of the operation. + progressProvider.Report(new ByteTransferProgress(totalRead, totalBytesToExtract)); + lastPercentage = progressPercentage; + } } } } diff --git a/HyperVExtension/src/HyperVExtension/Models/ByteTransferProgress.cs b/HyperVExtension/src/HyperVExtension/Models/ByteTransferProgress.cs new file mode 100644 index 0000000000..1cb1cf2b51 --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Models/ByteTransferProgress.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HyperVExtension.Models; + +/// +/// Represents progress of an operation that require transferring bytes from one place to another. +/// +public class ByteTransferProgress +{ + public long BytesReceived { get; set; } + + public long TotalBytesToReceive { get; set; } + + public uint PercentageComplete => (uint)((BytesReceived / (double)TotalBytesToReceive) * 100); + + public ByteTransferProgress(long bytesReceived, long totalBytesToReceive) + { + BytesReceived = bytesReceived; + TotalBytesToReceive = totalBytesToReceive; + } +} diff --git a/HyperVExtension/src/HyperVExtension/Models/VMGalleryCreationAdaptiveCardSession.cs b/HyperVExtension/src/HyperVExtension/Models/VMGalleryCreationAdaptiveCardSession.cs index 9ebeef4edb..7136b91b3f 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VMGalleryCreationAdaptiveCardSession.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VMGalleryCreationAdaptiveCardSession.cs @@ -90,7 +90,7 @@ public IAsyncOperation OnAction(string action, string i switch (_creationAdaptiveCard?.State) { case "initialCreationForm": - operationResult = await HandleActionWhenFormInInitalState(actionPayload, inputs); + operationResult = await HandleActionWhenFormInInitialState(actionPayload, inputs); break; case "reviewForm": (operationResult, shouldEndSession) = await HandleActionWhenFormInReviewState(actionPayload); @@ -158,16 +158,16 @@ private ProviderOperationResult GetInitialCreationFormAdaptiveCard() foreach (var image in _vMGalleryImageList.Images) { var dataJson = new JsonObject - { - { "ImageDescription", GetMergedDescription(image) }, - { "SubDescription", image.Publisher }, - { "Header", image.Name }, - { "HeaderIcon", image.Symbol.Base64Image }, - { "ActionButtonText", "More info" }, - { "ContentDialogInfo", SetupContentDialogInfo(image) }, - { "ButtonToLaunchContentDialogLabel", buttonToLaunchContentDialogLabel }, - { "SecondaryButtonForContentDialogText", secondaryButtonForContentDialogText }, - }; + { + { "ImageDescription", GetMergedDescription(image) }, + { "SubDescription", image.Publisher }, + { "Header", image.Name }, + { "HeaderIcon", image.Symbol.Base64Image }, + { "ActionButtonText", "More info" }, + { "ContentDialogInfo", SetupContentDialogInfo(image) }, + { "ButtonToLaunchContentDialogLabel", buttonToLaunchContentDialogLabel }, + { "SecondaryButtonForContentDialogText", secondaryButtonForContentDialogText }, + }; jsonArrayOfGalleryImages.Add(dataJson); } @@ -211,7 +211,7 @@ private async Task GetForReviewFormAdaptiveCardAsync(st } var galleryImage = _vMGalleryImageList.Images[inputForGalleryOperation.SelectedImageListIndex]; - var newVirtualMachineNameLabel = _stringResource.GetLocalized("NameLabelForNewVirtualMachine", ":"); + var newEnvironmentNameLabel = _stringResource.GetLocalized("NameLabelForNewVirtualMachine", ":"); var primaryButtonForCreationFlowText = _stringResource.GetLocalized("PrimaryButtonLabelForCreationFlow"); var secondaryButtonForCreationFlowText = _stringResource.GetLocalized("SecondaryButtonLabelForCreationFlow"); var storageFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(Constants.ExtensionIconInternal)); @@ -227,8 +227,8 @@ private async Task GetForReviewFormAdaptiveCardAsync(st { "DiskImageSize", BytesHelper.ConvertBytesToString(galleryImage.Disk.SizeInBytes) }, { "VMGalleryImageName", galleryImage.Name }, { "Publisher", galleryImage.Publisher }, - { "NameOfNewVM", inputForGalleryOperation.NewVirtualMachineName }, - { "NameLabel", newVirtualMachineNameLabel }, + { "NameOfNewVM", inputForGalleryOperation.NewEnvironmentName }, + { "NameLabel", newEnvironmentNameLabel }, { "Base64ImageForProvider", providerBase64Image }, { "DiskImageUrl", galleryImage.Symbol.Uri }, { "PrimaryButtonLabelForCreationFlow", primaryButtonForCreationFlowText }, @@ -247,7 +247,7 @@ private async Task GetForReviewFormAdaptiveCardAsync(st } /// - /// The discription for VM gallery images is stored in a list of strings. This method merges the strings into one string. + /// The description for VM gallery images is stored in a list of strings. This method merges the strings into one string. /// /// The c# class that represents the gallery image /// A string that combines the original list of strings into one @@ -298,7 +298,7 @@ private JsonObject SetupContentDialogInfo(VMGalleryImage image) }; } - private async Task HandleActionWhenFormInInitalState(AdaptiveCardActionPayload actionPayload, string inputs) + private async Task HandleActionWhenFormInInitialState(AdaptiveCardActionPayload actionPayload, string inputs) { ProviderOperationResult operationResult; var actionButtonId = actionPayload.Id ?? string.Empty; diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/ArchiveExtractionReport.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/ArchiveExtractionReport.cs index c5c5657e11..e606dacf64 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/ArchiveExtractionReport.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/ArchiveExtractionReport.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; + namespace HyperVExtension.Models.VirtualMachineCreation; /// @@ -10,15 +12,12 @@ public sealed class ArchiveExtractionReport : IOperationReport { public ReportKind ReportKind => ReportKind.ArchiveExtraction; - public string LocalizationKey => "ExtractingFile"; - - public ulong BytesReceived { get; private set; } + public string LocalizationKey => "ExtractionInProgress"; - public ulong TotalBytesToReceive { get; private set; } + public ByteTransferProgress ProgressObject { get; private set; } - public ArchiveExtractionReport(ulong bytesReceived, ulong totalBytesToReceive) + public ArchiveExtractionReport(ByteTransferProgress progressObj) { - BytesReceived = bytesReceived; - TotalBytesToReceive = totalBytesToReceive; + ProgressObject = progressObj; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DotNetZipArchiveProvider.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DotNetZipArchiveProvider.cs index 8c76087c49..073a2822ed 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DotNetZipArchiveProvider.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DotNetZipArchiveProvider.cs @@ -28,13 +28,13 @@ public async Task ExtractArchiveAsync(IProgress progressProvid using var outputFileStream = File.OpenWrite(destinationAbsoluteFilePath); using var zipArchiveEntryStream = zipArchiveEntry.Open(); - var fileExtractionProgress = new Progress(bytesCopied => + var fileExtractionProgress = new Progress(progressObj => { - progressProvider.Report(new ArchiveExtractionReport((ulong)bytesCopied, (ulong)totalBytesToExtract)); + progressProvider.Report(new ArchiveExtractionReport(progressObj)); }); outputFileStream.SetLength(totalBytesToExtract); - await zipArchiveEntryStream.CopyToAsync(outputFileStream, fileExtractionProgress, _transferBufferSize, cancellationToken); + await zipArchiveEntryStream.CopyToAsync(outputFileStream, fileExtractionProgress, _transferBufferSize, totalBytesToExtract, cancellationToken); File.SetLastWriteTime(destinationAbsoluteFilePath, zipArchiveEntry.LastWriteTime.DateTime); } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DownloadOperationReport.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DownloadOperationReport.cs index abec4f6301..53fe906262 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DownloadOperationReport.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DownloadOperationReport.cs @@ -9,13 +9,10 @@ public class DownloadOperationReport : IOperationReport public string LocalizationKey => "DownloadInProgress"; - public ulong BytesReceived { get; private set; } + public ByteTransferProgress ProgressObject { get; private set; } - public ulong TotalBytesToReceive { get; private set; } - - public DownloadOperationReport(ulong bytesReceived, ulong totalBytesToReceive) + public DownloadOperationReport(ByteTransferProgress progressObj) { - BytesReceived = bytesReceived; - TotalBytesToReceive = totalBytesToReceive; + ProgressObject = progressObj; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/IOperationReport.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/IOperationReport.cs index bf6bd3eb17..e418d3266a 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/IOperationReport.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/IOperationReport.cs @@ -15,7 +15,5 @@ public interface IOperationReport public string LocalizationKey { get; } - public ulong BytesReceived { get; } - - public ulong TotalBytesToReceive { get; } + public ByteTransferProgress ProgressObject { get; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryCreationUserInput.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryCreationUserInput.cs index d43193e2b5..73ecb39d1e 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryCreationUserInput.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryCreationUserInput.cs @@ -10,7 +10,7 @@ namespace HyperVExtension.Models.VirtualMachineCreation; /// public sealed class VMGalleryCreationUserInput { - public string NewVirtualMachineName { get; set; } = string.Empty; + public string NewEnvironmentName { get; set; } = string.Empty; [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public int SelectedImageListIndex { get; set; } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs index 64463af6dc..6e3202ff51 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs @@ -79,14 +79,7 @@ public VMGalleryVMCreationOperation( /// The archive extraction operation returned by the progress handler which extracts the archive file public void Report(IOperationReport value) { - var displayText = Image.Name; - - if (value.ReportKind == ReportKind.ArchiveExtraction) - { - displayText = $"{ArchivedFile!.Name} ({Image.Name})"; - } - - UpdateProgress(value, value.LocalizationKey, displayText); + UpdateProgress(value, value.LocalizationKey, $"({Image.Name})"); } /// @@ -114,6 +107,7 @@ public void Report(IOperationReport value) IsOperationInProgress = true; } + UpdateProgress(_stringResource.GetLocalized("CreationStarting", $"({_userInputParameters.NewEnvironmentName})")); var imageList = await _vmGalleryService.GetGalleryImagesAsync(); if (imageList.Images.Count == 0) { @@ -130,12 +124,12 @@ public void Report(IOperationReport value) var archiveProvider = _archiveProviderFactory.CreateArchiveProvider(ArchivedFile!.FileType); await archiveProvider.ExtractArchiveAsync(this, ArchivedFile!, absoluteFilePathForVhd, CancellationTokenSource.Token); - var virtualMachineName = MakeFileNameValid(_userInputParameters.NewVirtualMachineName); + var virtualMachineName = MakeFileNameValid(_userInputParameters.NewEnvironmentName); // Use the Hyper-V manager to create the VM. UpdateProgress(_stringResource.GetLocalized("CreationInProgress", virtualMachineName)); var creationParameters = new VirtualMachineCreationParameters( - _userInputParameters.NewVirtualMachineName, + _userInputParameters.NewEnvironmentName, GetVirtualMachineProcessorCount(), absoluteFilePathForVhd, Image.Config.SecureBoot, @@ -158,16 +152,29 @@ public void Report(IOperationReport value) private void UpdateProgress(IOperationReport report, string localizedKey, string fileName) { - var bytesReceivedSoFar = BytesHelper.ConvertBytesToString(report.BytesReceived); - var totalBytesToReceive = BytesHelper.ConvertBytesToString(report.TotalBytesToReceive); - var progressPercentage = (uint)((report.BytesReceived / (double)report.TotalBytesToReceive) * 100D); - var displayString = _stringResource.GetLocalized(localizedKey, fileName, $"{bytesReceivedSoFar}/{totalBytesToReceive}"); - Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(displayString, progressPercentage)); + var bytesReceivedSoFar = BytesHelper.ConvertBytesToString((ulong)report.ProgressObject.BytesReceived); + var totalBytesToReceive = BytesHelper.ConvertBytesToString((ulong)report.ProgressObject.TotalBytesToReceive); + var displayString = _stringResource.GetLocalized(localizedKey, fileName, $"{bytesReceivedSoFar} / {totalBytesToReceive}"); + try + { + Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(displayString, report.ProgressObject.PercentageComplete)); + } + catch (Exception ex) + { + _log.Error("Failed to update progress", ex); + } } private void UpdateProgress(string localizedString, uint percentage = 0u) { - Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(localizedString, percentage)); + try + { + Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(localizedString, percentage)); + } + catch (Exception ex) + { + _log.Error("Failed to update progress", ex); + } } /// @@ -227,7 +234,7 @@ private string MakeFileNameValid(string originalName) private string GetUniqueAbsoluteFilePath(string defaultVirtualDiskPath) { var extension = Path.GetExtension(Image.Disk.ArchiveRelativePath); - var expectedExtractedFileLocation = Path.Combine(defaultVirtualDiskPath, $"{_userInputParameters.NewVirtualMachineName}{extension}"); + var expectedExtractedFileLocation = Path.Combine(defaultVirtualDiskPath, $"{_userInputParameters.NewEnvironmentName}{extension}"); var appendedNumber = 1u; var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(expectedExtractedFileLocation); diff --git a/HyperVExtension/src/HyperVExtension/Services/DownloaderService.cs b/HyperVExtension/src/HyperVExtension/Services/DownloaderService.cs index 6593a7adfc..dda443863d 100644 --- a/HyperVExtension/src/HyperVExtension/Services/DownloaderService.cs +++ b/HyperVExtension/src/HyperVExtension/Services/DownloaderService.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using HyperVExtension.Extensions; +using HyperVExtension.Models; using HyperVExtension.Models.VirtualMachineCreation; namespace HyperVExtension.Services; @@ -32,13 +33,12 @@ public async Task StartDownloadAsync(IProgress progressProvide using var outputFileStream = File.OpenWrite(destinationFile); outputFileStream.SetLength(totalBytesToReceive); - var downloadProgress = new Progress(bytesCopied => + var downloadProgress = new Progress(progressObj => { - var percentage = (uint)(bytesCopied / (double)totalBytesToReceive * 100D); - progressProvider.Report(new DownloadOperationReport((ulong)bytesCopied, (ulong)totalBytesToReceive)); + progressProvider.Report(new DownloadOperationReport(progressObj)); }); - await webFileStream.CopyToAsync(outputFileStream, downloadProgress, _transferBufferSize, cancellationToken); + await webFileStream.CopyToAsync(outputFileStream, downloadProgress, _transferBufferSize, totalBytesToReceive, cancellationToken); } /// diff --git a/HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw b/HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw index 2c0fd6dfa5..7f7aa770ac 100644 --- a/HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw +++ b/HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw @@ -118,8 +118,12 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Creating: {0} - Locked="{0}" text to tell the user that we're currently creating the virtual machine. {0} is the name of the virtual machine + Adding network switch, secure boot and enhanced session configuration for {0} + Locked="{0}" Text to tell the user that we're performing post creation actions like adding a network switch to the virtual machine. {0} is the name of the virtual machine + + + Starting the creation process for {0} + Locked="{0}" Text to tell the user that we're starting the process to create the virtual machine. {0} is the name of the virtual machine Current Checkpoint @@ -130,7 +134,7 @@ Locked="{0}" text to tell the user that a file exists and we do not need to download it again. {0} is a previously download file. We show the file name in {0}. - Downloading {0}. {1} + Downloading {0} {1} Locked="{0}" text to tell the user that we are downloading a file from the web. {0} is the file we're downloading. {1} the progress in the form of "bytes received / total bytes needed". E.g "10 Mb / 400 Mb" @@ -158,7 +162,7 @@ Attempt counter text for the dialog to enter Hyper-V VM admin credential ({CurrentAttempt}/{MaxAttempts}). - Extracting file {0}. {1} + Extracting file {0} {1} Locked="{0}" text to tell the user that we're extracting a zip file into a location on their computer. {0} is the zip file we're extracting. {1} the progress in the form of "bytes extracted / total bytes needed". E.g "10 Mb / 400 Mb" diff --git a/HyperVExtension/src/HyperVExtension/Templates/InitialVMGalleryCreationForm.json b/HyperVExtension/src/HyperVExtension/Templates/InitialVMGalleryCreationForm.json index 95e64d78ba..31a9d10b4a 100644 --- a/HyperVExtension/src/HyperVExtension/Templates/InitialVMGalleryCreationForm.json +++ b/HyperVExtension/src/HyperVExtension/Templates/InitialVMGalleryCreationForm.json @@ -5,7 +5,7 @@ "body": [ { "type": "Input.Text", - "id": "NewVirtualMachineName", + "id": "NewEnvironmentName", "label": "${EnterNewVMNameLabel}", "placeholder": "${EnterNewVMNamePlaceHolder}", "maxLength": 100, diff --git a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVExtensionIntegrationTest.cs b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVExtensionIntegrationTest.cs index 97a0aaf1ce..8de473bab9 100644 --- a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVExtensionIntegrationTest.cs +++ b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVExtensionIntegrationTest.cs @@ -393,7 +393,7 @@ public async Task TestVirtualMachineCreationFromVmGallery() var smallestImageIndex = await GetIndexOfImageWithSmallestRequiredSpace(imageList); var inputJson = JsonSerializer.Serialize(new VMGalleryCreationUserInput() { - NewVirtualMachineName = expectedVMName, + NewEnvironmentName = expectedVMName, // Get Image with the smallest size from gallery, we'll use it to create a VM. SelectedImageListIndex = smallestImageIndex, diff --git a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVProviderTests.cs b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVProviderTests.cs index 68ca35480d..68a62af01c 100644 --- a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVProviderTests.cs +++ b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVProviderTests.cs @@ -77,7 +77,7 @@ public async Task HyperVProvider_Can_Create_VirtualMachine() var hyperVProvider = TestHost!.GetService(); var inputJson = JsonSerializer.Serialize(new VMGalleryCreationUserInput() { - NewVirtualMachineName = _expectedVmName, + NewEnvironmentName = _expectedVmName, SelectedImageListIndex = 0, // Our test gallery image list Json only has one image }); diff --git a/HyperVExtension/test/HyperVExtension/Mocks/DownloaderServiceMock.cs b/HyperVExtension/test/HyperVExtension/Mocks/DownloaderServiceMock.cs index 712f7cf5a3..52c8134a33 100644 --- a/HyperVExtension/test/HyperVExtension/Mocks/DownloaderServiceMock.cs +++ b/HyperVExtension/test/HyperVExtension/Mocks/DownloaderServiceMock.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using HyperVExtension.Models; using HyperVExtension.Models.VirtualMachineCreation; using HyperVExtension.Services; using Windows.Storage; @@ -11,9 +12,9 @@ public class DownloaderServiceMock : IDownloaderService { private readonly int _totalIterations = 4; - private readonly ulong _totalBytesToReceive = 1000; + private readonly long _totalBytesToReceive = 1000; - private readonly ulong _bytesReceivedEachIteration = 250; + private readonly long _bytesReceivedEachIteration = 250; private readonly IHttpClientFactory _httpClientFactory; @@ -24,12 +25,12 @@ public DownloaderServiceMock(IHttpClientFactory httpClientFactory) public async Task StartDownloadAsync(IProgress progressProvider, Uri sourceWebUri, string destinationFile, CancellationToken cancellationToken) { - var bytesReceivedSoFar = 0UL; + var bytesReceivedSoFar = 0L; for (var i = 0; i < _totalIterations; i++) { await Task.Delay(100, cancellationToken); bytesReceivedSoFar += _bytesReceivedEachIteration; - progressProvider.Report(new DownloadOperationReport(bytesReceivedSoFar, _totalBytesToReceive)); + progressProvider.Report(new DownloadOperationReport(new ByteTransferProgress(bytesReceivedSoFar, _totalBytesToReceive))); } var zipFile = await GetTestZipFileInPackage(); diff --git a/common/Environments/Converters/CardStateColorToBrushConverter.cs b/common/Environments/Converters/CardStateColorToBrushConverter.cs index 0ac1303b9b..ca01704c09 100644 --- a/common/Environments/Converters/CardStateColorToBrushConverter.cs +++ b/common/Environments/Converters/CardStateColorToBrushConverter.cs @@ -25,6 +25,7 @@ public object Convert(object value, Type targetType, object parameter, string la CardStateColor.Success => (SolidColorBrush)Application.Current.Resources["SystemFillColorSuccessBrush"], CardStateColor.Neutral => (SolidColorBrush)Application.Current.Resources["SystemFillColorSolidNeutralBrush"], CardStateColor.Caution => (SolidColorBrush)Application.Current.Resources["SystemFillColorCautionBrush"], + CardStateColor.Failure => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"], _ => (SolidColorBrush)Application.Current.Resources["SystemFillColorCautionBrush"], }; } diff --git a/common/Environments/CustomControls/CardBody.xaml b/common/Environments/CustomControls/CardBody.xaml index 5fa356c1ff..5c72231e56 100644 --- a/common/Environments/CustomControls/CardBody.xaml +++ b/common/Environments/CustomControls/CardBody.xaml @@ -7,10 +7,14 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:devEnvConverters="using:DevHome.Common.Environments.Converters" xmlns:controls="using:CommunityToolkit.WinUI.Controls" + xmlns:converters="using:CommunityToolkit.WinUI.Converters" mc:Ignorable="d"> + + + - + - - - - + + + + - + - + + @@ -81,6 +95,10 @@ HorizontalSpacing="15"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -118,8 +190,23 @@ + + @@ -142,11 +229,14 @@ + ItemsSource="{x:Bind ViewModel.Providers, Mode=OneWay}" + SelectedIndex="{x:Bind ViewModel.SelectedProviderIndex, Mode=TwoWay}"> - - + + @@ -191,44 +281,15 @@ + MaxWidth="{ThemeResource MaxPageContentWidth}" + ItemsSource="{x:Bind ViewModel.ComputeSystemCardsView}" SelectionMode="None" + ItemTemplateSelector="{StaticResource CardItemTemplateSelector}" + ItemContainerStyle="{StaticResource HorizontalCardListViewItemContainerForManagementPageStyle}"> - - - - - - - - - - - - - - - - - - - - + (Visibility)GetValue(HeaderVisibilityProperty); set => SetValue(HeaderVisibilityProperty, value); } + + public Visibility ContentVisibility + { + get => (Visibility)GetValue(ContentVisibilityProperty); + set => SetValue(ContentVisibilityProperty, value); + } public SetupShell() { @@ -66,4 +72,5 @@ public SetupShell() public static readonly DependencyProperty HeaderProperty = DependencyProperty.RegisterAttached(nameof(Header), typeof(object), typeof(SetupShell), new PropertyMetadata(null)); public static readonly DependencyProperty OrchestratorProperty = DependencyProperty.RegisterAttached(nameof(Orchestrator), typeof(SetupFlowOrchestrator), typeof(SetupShell), new PropertyMetadata(null)); public static readonly DependencyProperty HeaderVisibilityProperty = DependencyProperty.RegisterAttached(nameof(HeaderVisibility), typeof(Visibility), typeof(SetupShell), new PropertyMetadata(Visibility.Visible)); + public static readonly DependencyProperty ContentVisibilityProperty = DependencyProperty.RegisterAttached(nameof(ContentVisibility), typeof(Visibility), typeof(SetupShell), new PropertyMetadata(Visibility.Visible)); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs new file mode 100644 index 0000000000..41d541f11e --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +extern alias Projection; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.Common.Environments.Models; +using DevHome.Common.Environments.Services; +using DevHome.Common.Models; +using DevHome.SetupFlow.Models.Environments; +using DevHome.SetupFlow.Services; +using DevHome.SetupFlow.ViewModels; +using Projection::DevHome.SetupFlow.ElevatedComponent; +using Serilog; +using Windows.Foundation; +using DevHomeSDK = Microsoft.Windows.DevHome.SDK; + +namespace DevHome.SetupFlow.Models; + +/// +/// Task that creates an environment using the user input from an adaptive card session. +/// +public sealed class CreateEnvironmentTask : ISetupTask, IDisposable, IRecipient +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(CreateEnvironmentTask)); + + private readonly IComputeSystemManager _computeSystemManager; + + private readonly TaskMessages _taskMessages; + + private readonly ActionCenterMessages _actionCenterMessages = new(); + + private readonly ISetupFlowStringResource _stringResource; + + // Used to signal the task to start the creation operation. This is used when the adaptive card session ends. + private readonly AutoResetEvent _autoResetEventToStartCreationOperation = new(false); + + private readonly SetupFlowViewModel _setupFlowViewModel; + + private bool _disposedValue; + + public event ISetupTask.ChangeMessageHandler AddMessage; + + public string UserJsonInput { get; set; } + + public ComputeSystemProviderDetails ProviderDetails { get; set; } + + public DeveloperIdWrapper DeveloperIdWrapper { get; set; } + + // The "#pragma warning disable 67" directive suppresses the CS0067 warning. + // CS0067 is a warning that occurs when a public event is declared but never used. +#pragma warning disable 67 + public event ISetupTask.ChangeActionCenterMessageHandler UpdateActionCenterMessage; +#pragma warning restore 67 + + public bool RequiresAdmin => false; + + public bool RequiresReboot => false; + + public bool DependsOnDevDriveToBeInstalled => false; + + public bool CreationOperationStarted { get; private set; } + + public ISummaryInformationViewModel SummaryScreenInformation { get; } + + public CreateEnvironmentTask(IComputeSystemManager computeSystemManager, ISetupFlowStringResource stringResource, SetupFlowViewModel setupFlowViewModel) + { + _computeSystemManager = computeSystemManager; + _stringResource = stringResource; + _taskMessages = new TaskMessages + { + Executing = _stringResource.GetLocalized(StringResourceKey.StartingEnvironmentCreation), + Finished = _stringResource.GetLocalized(StringResourceKey.EnvironmentCreationOperationInitializationFinished), + Error = _stringResource.GetLocalized(StringResourceKey.EnvironmentCreationError), + }; + _setupFlowViewModel = setupFlowViewModel; + _setupFlowViewModel.EndSetupFlow += OnEndSetupFlow; + + // Register for the adaptive card session ended message so we can use the session data to create the environment + WeakReferenceMessenger.Default.Register(this); + } + + public ActionCenterMessages GetErrorMessages() => _actionCenterMessages; + + public TaskMessages GetLoadingMessages() => _taskMessages; + + public ActionCenterMessages GetRebootMessage() => new(); + + /// + /// Receives the adaptive card session ended message from the he + /// once the extension sends the session ended event. + /// + /// + /// The message payload that contains the provider and the user input json that will be used to invoke the + /// + /// + public void Receive(CreationAdaptiveCardSessionEndedMessage message) + { + _log.Information("The extension sent the session ended event"); + ProviderDetails = message.Value.ProviderDetails; + + // Json input that the user entered in the adaptive card session + UserJsonInput = message.Value.UserInputResultJson; + + // In the future we'll add the specific developer ID to the task, but for now since we haven't + // add support for switching between developer Id's in the environments pages, we'll use the first one + // in the provider details list of developer IDs. If we get here, then there should be at least one. + DeveloperIdWrapper = message.Value.ProviderDetails.DeveloperIds.First(); + + _log.Information("Signaling to the waiting event handle to Continue the 'Execute' operation"); + _autoResetEventToStartCreationOperation.Set(); + } + + private void OnEndSetupFlow(object sender, EventArgs e) + { + WeakReferenceMessenger.Default.Unregister(this); + _setupFlowViewModel.EndSetupFlow -= OnEndSetupFlow; + } + + IAsyncOperation ISetupTask.Execute() + { + return Task.Run(() => + { + _log.Information("Executing the operation. Waiting to be signalled that the adaptive card session has ended"); + + // Either wait until we're signaled to continue execution or we times out after 1 minute. If this task is initiated + // then that means the user went past the review page. At this point the extension should be firing a session ended + // event. Since the call flow is disjointed an extension may not have sent the session ended event when this method is called. + _autoResetEventToStartCreationOperation.WaitOne(TimeSpan.FromMinutes(1)); + + if (string.IsNullOrWhiteSpace(UserJsonInput)) + { + // The extension's creation adaptive card may not need user input. In that case, the user input will be null or empty. + _log.Information("UserJsonInput is null or empty."); + } + + // If the provider details are null, then we can't proceed with the operation. This happens if the auto event times out. + if (ProviderDetails == null) + { + _log.Error("ProviderDetails is null so we cannot proceed with executing the task"); + AddMessage(_stringResource.GetLocalized(StringResourceKey.EnvironmentCreationFailedToGetProviderInformation), MessageSeverityKind.Error); + return TaskFinishedState.Failure; + } + + var sdkCreateEnvironmentOperation = ProviderDetails.ComputeSystemProvider.CreateCreateComputeSystemOperation(DeveloperIdWrapper.DeveloperId, UserJsonInput); + var createComputeSystemOperationWrapper = new CreateComputeSystemOperation(sdkCreateEnvironmentOperation, ProviderDetails, UserJsonInput); + + // Start the operation, which returns immediately and runs in the background. + createComputeSystemOperationWrapper.StartOperation(); + AddMessage(_stringResource.GetLocalized(StringResourceKey.EnvironmentCreationForProviderStarted), MessageSeverityKind.Info); + + _computeSystemManager.AddRunningOperationForCreation(createComputeSystemOperationWrapper); + CreationOperationStarted = true; + + _log.Information("Successfully started the creation operation"); + return TaskFinishedState.Success; + }).AsAsyncOperation(); + } + + IAsyncOperation ISetupTask.ExecuteAsAdmin(IElevatedComponentOperation elevatedComponentOperation) + { + return Task.Run(() => + { + // No admin rights required for this task. This shouldn't ever be invoked since the RequiresAdmin property is always false. + _log.Error("Admin execution is not required for the create environment task"); + return TaskFinishedState.Failure; + }).AsAsyncOperation(); + } + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _autoResetEventToStartCreationOperation.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs index 955e524fe5..93469e207a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs @@ -27,6 +27,7 @@ public enum SetupFlowKind { LocalMachine, SetupTarget, + CreateEnvironment, } /// diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs index 81050c7075..9535a5eaaa 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs @@ -266,5 +266,23 @@ public static class StringResourceKey public static readonly string SelectEnvironmentPageTitle = nameof(SelectEnvironmentPageTitle); public static readonly string ConfigureEnvironmentPageTitle = nameof(ConfigureEnvironmentPageTitle); public static readonly string EnvironmentCreationReviewPageTitle = nameof(EnvironmentCreationReviewPageTitle); + public static readonly string EnvironmentCreationReviewTabTitle = nameof(EnvironmentCreationReviewTabTitle); + public static readonly string EnvironmentCreationError = nameof(EnvironmentCreationError); + public static readonly string StartingEnvironmentCreation = nameof(StartingEnvironmentCreation); + public static readonly string EnvironmentCreationOperationInitializationFinished = nameof(EnvironmentCreationOperationInitializationFinished); + public static readonly string EnvironmentCreationForProviderStarted = nameof(EnvironmentCreationForProviderStarted); + public static readonly string EnvironmentCreationFailedToGetProviderInformation = nameof(EnvironmentCreationFailedToGetProviderInformation); + public static readonly string EnvironmentCreationReviewExpanderDescription = nameof(EnvironmentCreationReviewExpanderDescription); public static readonly string CreateEnvironmentButtonText = nameof(CreateEnvironmentButtonText); + public static readonly string SetupShellReviewPageDescriptionForEnvironmentCreation = nameof(SetupShellReviewPageDescriptionForEnvironmentCreation); + + // Summary page + public static readonly string SummaryPageOpenDashboard = nameof(SummaryPageOpenDashboard); + public static readonly string SummaryPageRedirectToEnvironmentPageButton = nameof(SummaryPageRedirectToEnvironmentPageButton); + public static readonly string SummaryPageHeader = nameof(SummaryPageHeader); + public static readonly string SummaryPageHeaderForEnvironmentCreationText = nameof(SummaryPageHeaderForEnvironmentCreationText); + + // Review page + public static readonly string ReviewExpanderDescription = nameof(ReviewExpanderDescription); + public static readonly string SetupShellReviewPageDescription = nameof(SetupShellReviewPageDescription); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw index 92b4555a2e..6bee36d1ba 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -649,7 +649,7 @@ Generate a WinGet Configuration file (.winget) to repeat this set up in the future or share it with others. {Locked="WinGet",".winget"}Tooltip text about the generated configuration file - + Set up details Header for a section detailing the set up steps to be performed. "Set up" is the noun @@ -713,10 +713,14 @@ Select a target machine to set up. Description for the setup target page - + Review the terms and setup details below before applying these changes to your computer. Description for the review page + + Review the details for your new environment + Description for the review page + Applications Header for a section showing a summary of applications to be installed @@ -1044,7 +1048,7 @@ Installed applications Header for the section that shows all the downloaded apps - + Here's what we set up for you Header text for the summary page @@ -1056,7 +1060,7 @@ Next steps Text for the "Next steps" section of the summary page - + Open Dashboard Button content to let user go to the dashboard @@ -1764,12 +1768,59 @@ There was an error retreiving the adaptive card session from the extension Error text display when we are unable to retrieve the adaptive card information from an extension + + Environment + Title for create environment review tab + + + Your environment's details + Title for create environment review tab + - Create Environment + Review your environment Title for create environment review page Create Environment - Text for button that starts creating a new virtual machine or cloud pc for the user + + + There was an error starting the creation operation + Text to tell the user that we couldn't start the operation to create their local or cloud virtual environment + + + The operation to create your environment has started successfully + Text to tell the user that we were able to start the operation that create their local or cloud virtual machine successfully + + + Starting the create environment operation + Text to tell the user that the operation to create their local or cloud virtual environment has started + + + We timed out waiting for the extension to provide us with information to create your environment + Error text to show the user that we timed out while waiting for a response from a Dev Home extension + + + The {0} provider is now creating your environment + {Locked="{0}"} Text that tells the user that a specific provider has started creating their environment. {0} is the name of a provider who Dev Home will send the request to. + + + We failed to start the creation operation for the {0} provider + {Locked="{0}"} Text that tells the user that Dev Home could not start the creation operation for a specific provider. {0} is the name of a provider who Dev Home will send the request to. + + + Environment details + Heading that will show the user the details of the environment they initiated the creation process for. Environments can be local or cloud virtual machines + + + Go to Environments page + Text for button that when clicked will redirect the user to the environments page in Dev Home + + + Environment being created + Text to tell the user that an environment is being created. Environments can be local or cloud virtual machines + + + We've started creating your environment + Text to tell the user that an environment is being created. Environments can be local or cloud virtual machines \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/EnvironmentCreationOptionsTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/EnvironmentCreationOptionsTaskGroup.cs index 7661f0361e..130377a53d 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/EnvironmentCreationOptionsTaskGroup.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/EnvironmentCreationOptionsTaskGroup.cs @@ -23,6 +23,8 @@ public class EnvironmentCreationOptionsTaskGroup : ISetupTaskGroup public ComputeSystemProviderDetails ProviderDetails { get; private set; } + public CreateEnvironmentTask CreateEnvironmentTask { get; private set; } + public EnvironmentCreationOptionsTaskGroup( SetupFlowViewModel setupFlowViewModel, IComputeSystemManager computeSystemManager, @@ -34,9 +36,10 @@ public EnvironmentCreationOptionsTaskGroup( _createEnvironmentReviewViewModel = createEnvironmentReviewViewModel; _setupFlowStringResource = setupFlowStringResource; _computeSystemManager = computeSystemManager; + CreateEnvironmentTask = new CreateEnvironmentTask(_computeSystemManager, _setupFlowStringResource, setupFlowViewModel); } - public IEnumerable SetupTasks => new List(); + public IEnumerable SetupTasks => new List() { CreateEnvironmentTask }; public IEnumerable DSCTasks => new List(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/CreateEnvironmentReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/CreateEnvironmentReviewViewModel.cs index a735e3f15a..543ef7d544 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/CreateEnvironmentReviewViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/CreateEnvironmentReviewViewModel.cs @@ -16,6 +16,6 @@ public CreateEnvironmentReviewViewModel( ISetupFlowStringResource stringResource) { _stringResource = stringResource; - TabTitle = stringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewPageTitle); + TabTitle = stringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewTabTitle); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs index 11a62a5522..38fa6699a3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs @@ -91,6 +91,7 @@ public EnvironmentCreationOptionsViewModel( _elementRegistration.Set(DevHomeLaunchContentDialogButton.AdaptiveElementType, new DevHomeLaunchContentDialogButtonParser()); _elementRegistration.Set(DevHomeContentDialogContent.AdaptiveElementType, new DevHomeContentDialogContentParser()); _adaptiveCardRenderingService = renderingService; + Orchestrator.CurrentSetupFlowKind = SetupFlowKind.CreateEnvironment; } /// @@ -109,6 +110,7 @@ private void OnEndSetupFlow(object sender, EventArgs e) ResetAdaptiveCardConfiguration(); WeakReferenceMessenger.Default.UnregisterAll(this); _setupFlowViewModel.EndSetupFlow -= OnEndSetupFlow; + Orchestrator.CurrentSetupFlowKind = SetupFlowKind.LocalMachine; } protected async override Task OnFirstNavigateToAsync() @@ -267,7 +269,7 @@ private void OnReviewPageViewRequest(EnvironmentCreationOptionsViewModel recipie { // Only send the adaptive card if the session has loaded. If the session hasn't loaded yet, we'll send an empty response. The review page should be sent the adaptive card // once the session has loaded in the OnAdaptiveCardUpdated method. - if (!IsAdaptiveCardSessionLoaded) + if (!IsAdaptiveCardSessionLoaded && Orchestrator?.CurrentPageViewModel is not SummaryViewModel) { return; } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs index 4ac0ecf9fb..dedd93a429 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs @@ -49,7 +49,6 @@ private async Task LoadProvidersAsync() Orchestrator.NotifyNavigationCanExecuteChanged(); var providerDetails = await Task.Run(_computeSystemService.GetComputeSystemProvidersAsync); - ProvidersViewModels = new(); foreach (var providerDetail in providerDetails) { diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs index 9e1f69ffdd..76a5319861 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs @@ -192,12 +192,12 @@ private void StartRepoConfig(string flowTitle) /// Starts the create environment flow. /// [RelayCommand] - private void StartCreateEnvironment(string flowTitle) + public void StartCreateEnvironment(string flowTitle) { _log.Information("Starting flow for environment creation"); StartSetupFlowForTaskGroups( flowTitle, - "RepoConfig", + "CreateEnvironment", _host.GetService(), _host.GetService()); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs index 7f9ef63faa..1129c1b62f 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs @@ -41,6 +41,15 @@ public partial class ReviewViewModel : SetupPageViewModelBase [NotifyCanExecuteChangedFor(nameof(SetUpCommand))] private bool _canSetUp; + [ObservableProperty] + private string _reviewPageTitle; + + [ObservableProperty] + private string _reviewPageExpanderDescription; + + [ObservableProperty] + private string _reviewPageDescription; + public bool HasApplicationsToInstall => Orchestrator.GetTaskGroup()?.SetupTasks.Any() == true; public bool RequiresTermsAgreement => HasApplicationsToInstall; @@ -88,6 +97,8 @@ public ReviewViewModel( { NextPageButtonText = StringResource.GetLocalized(StringResourceKey.SetUpButton); PageTitle = StringResource.GetLocalized(StringResourceKey.ReviewPageTitle); + ReviewPageExpanderDescription = StringResource.GetLocalized(StringResourceKey.ReviewExpanderDescription); + ReviewPageDescription = StringResource.GetLocalized(StringResourceKey.SetupShellReviewPageDescription); _setupFlowOrchestrator = orchestrator; _configFileBuilder = configFileBuilder; @@ -104,7 +115,18 @@ protected async override Task OnEachNavigateToAsync() .ToList(); SelectedReviewTab = ReviewTabs.FirstOrDefault(); + // If the CreateEnvironmentTaskGroup is present, update the setup button text to "Create Environment" + // and page title to "Review your environment" + if (Orchestrator.GetTaskGroup() != null) + { + NextPageButtonText = StringResource.GetLocalized(StringResourceKey.CreateEnvironmentButtonText); + PageTitle = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewPageTitle); + ReviewPageExpanderDescription = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewExpanderDescription); + ReviewPageDescription = StringResource.GetLocalized(StringResourceKey.SetupShellReviewPageDescriptionForEnvironmentCreation); + } + NextPageButtonToolTipText = HasTasksToSetUp ? null : StringResource.GetLocalized(StringResourceKey.ReviewNothingToSetUpToolTip); + UpdateCanSetUp(); await Task.CompletedTask; diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs index f6f3b23546..feba7c037b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs @@ -14,6 +14,7 @@ using DevHome.SetupFlow.Services; using DevHome.Telemetry; using Microsoft.Extensions.Hosting; +using Microsoft.UI.Xaml.Navigation; using Serilog; using Windows.Storage; @@ -26,6 +27,8 @@ public partial class SetupFlowViewModel : ObservableObject private readonly MainPageViewModel _mainPageViewModel; private readonly PackageProvider _packageProvider; + private readonly string _creationFlowNavigationParameter = "StartCreationFlow"; + public SetupFlowOrchestrator Orchestrator { get; } public event EventHandler EndSetupFlow = (s, e) => { }; @@ -119,4 +122,25 @@ public async Task StartFileActivationFlowAsync(StorageFile file) Orchestrator.FlowPages = [_mainPageViewModel]; await _mainPageViewModel.StartConfigurationFileAsync(file); } + + public void StartCreationFlowAsync() + { + Orchestrator.FlowPages = [_mainPageViewModel]; + _mainPageViewModel.StartCreateEnvironment(string.Empty); + } + + public void OnNavigatedTo(NavigationEventArgs args) + { + // The setup flow isn't setup to support using the navigation service to navigate to specific + // pages. Instead we need to navigate to the main page and then start the creation flow template manually. + var parameter = args.Parameter?.ToString(); + + if ((!string.IsNullOrEmpty(parameter)) && + _creationFlowNavigationParameter.Equals(parameter, StringComparison.OrdinalIgnoreCase) && + Orchestrator.CurrentSetupFlowKind != SetupFlowKind.CreateEnvironment) + { + Cancel(); + StartCreationFlowAsync(); + } + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs index 884831843e..b6e45248dc 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs @@ -108,6 +108,20 @@ public ObservableCollection AppsDownloaded } } + public bool WasCreateEnvironmentOperationStarted + { + get + { + var taskGroup = Orchestrator.GetTaskGroup(); + if (taskGroup == null) + { + return false; + } + + return taskGroup.CreateEnvironmentTask.CreationOperationStarted; + } + } + public List AppsDownloadedInstallationNotes => AppsDownloaded.Where(p => !string.IsNullOrEmpty(p.InstallationNotes)).ToList(); public IList ConfigurationUnitResults => _configurationUnitResults.Value; @@ -134,6 +148,9 @@ public ObservableCollection AppsDownloaded [ObservableProperty] private string _applicationsClonedText; + [ObservableProperty] + private string _summaryPageEnvironmentCreatingText; + [RelayCommand] public void RemoveRestartGrid() { @@ -157,7 +174,6 @@ public void GoToMainPage() _setupFlowViewModel.TerminateCurrentFlow("Summary_GoToMainPage"); } - [RelayCommand] public void GoToDashboard() { TelemetryFactory.Get().Log("Summary_NavigateTo_Event", LogLevel.Critical, new NavigateFromSummaryEvent("Dashboard"), Orchestrator.ActivityId); @@ -165,6 +181,26 @@ public void GoToDashboard() _setupFlowViewModel.TerminateCurrentFlow("Summary_GoToDashboard"); } + [RelayCommand] + public void RedirectToNextPage() + { + if (WasCreateEnvironmentOperationStarted) + { + GoToEnvironmentsPage(); + return; + } + + // Default behavior is to go to the dashboard + GoToDashboard(); + } + + public void GoToEnvironmentsPage() + { + TelemetryFactory.Get().Log("Summary_NavigateTo_Event", LogLevel.Critical, new NavigateFromSummaryEvent("Environments"), Orchestrator.ActivityId); + _host.GetService().NavigateTo(KnownPageKeys.Environments); + _setupFlowViewModel.TerminateCurrentFlow("Summary_GoToEnvironments"); + } + [RelayCommand] public void GoToDevHomeSettings() { @@ -180,6 +216,12 @@ public void GoToForDevelopersSettingsPage() Task.Run(() => Launcher.LaunchUriAsync(new Uri("ms-settings:developers"))).Wait(); } + [ObservableProperty] + private string _pageRedirectButtonText; + + [ObservableProperty] + private string _pageHeaderText; + public SummaryViewModel( ISetupFlowStringResource stringResource, SetupFlowOrchestrator orchestrator, @@ -199,6 +241,8 @@ public SummaryViewModel( _showRestartNeeded = Visibility.Collapsed; _appManagementInitializer = appManagementInitializer; _cloneRepoNextSteps = new(); + PageRedirectButtonText = StringResource.GetLocalized(StringResourceKey.SummaryPageOpenDashboard); + PageHeaderText = StringResource.GetLocalized(StringResourceKey.SummaryPageHeader); IsNavigationBarVisible = true; IsStepPage = false; @@ -259,6 +303,12 @@ protected async override Task OnFirstNavigateToAsync() TelemetryFactory.Get().LogCritical("Summary_NavigatedTo_Event", false, Orchestrator.ActivityId); } + if (WasCreateEnvironmentOperationStarted) + { + PageRedirectButtonText = StringResource.GetLocalized(StringResourceKey.SummaryPageRedirectToEnvironmentPageButton); + PageHeaderText = StringResource.GetLocalized(StringResourceKey.SummaryPageHeaderForEnvironmentCreationText); + } + await ReloadCatalogsAsync(); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml index d987ef1f54..fa967365f7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml @@ -21,15 +21,17 @@ + Grid.Row="0" + ContentVisibility="Collapsed"> - + @@ -55,7 +57,7 @@ Height="25" Margin="0,150,0,20"/> - + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml index 659e66afa5..30efe7f7f5 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml @@ -38,10 +38,12 @@ + Grid.Row="0" + ContentVisibility="Collapsed"> - + + Style="{StaticResource BodyStrongTextBlockStyle}" + Text="{x:Bind ViewModel.ReviewPageExpanderDescription, Mode=OneWay}" /> From a4c182986e2265670d42c9177120e567d99afa84 Mon Sep 17 00:00:00 2001 From: David Bennett Date: Tue, 9 Apr 2024 11:05:44 -0700 Subject: [PATCH 077/121] Fix timestamp log format (#2568) --- .../src/DevSetupAgent/appsettings_hypervsetupagent.json | 4 ++-- .../src/DevSetupEngine/appsettings_hypervsetup.json | 4 ++-- .../src/HyperVExtensionServer/appsettings_hyperv.json | 4 ++-- extensions/CoreWidgetProvider/corewidgets_appsettings.json | 4 ++-- src/appsettings.json | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/HyperVExtension/src/DevSetupAgent/appsettings_hypervsetupagent.json b/HyperVExtension/src/DevSetupAgent/appsettings_hypervsetupagent.json index e5932df92c..b66927f704 100644 --- a/HyperVExtension/src/DevSetupAgent/appsettings_hypervsetupagent.json +++ b/HyperVExtension/src/DevSetupAgent/appsettings_hypervsetupagent.json @@ -12,7 +12,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -20,7 +20,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\hyperv_setupagent.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/HyperVExtension/src/DevSetupEngine/appsettings_hypervsetup.json b/HyperVExtension/src/DevSetupEngine/appsettings_hypervsetup.json index 9653f50282..ce815ea0b5 100644 --- a/HyperVExtension/src/DevSetupEngine/appsettings_hypervsetup.json +++ b/HyperVExtension/src/DevSetupEngine/appsettings_hypervsetup.json @@ -6,7 +6,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -14,7 +14,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\hyperv_setup.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/HyperVExtension/src/HyperVExtensionServer/appsettings_hyperv.json b/HyperVExtension/src/HyperVExtensionServer/appsettings_hyperv.json index 15a4f5fade..f79a3cf6d0 100644 --- a/HyperVExtension/src/HyperVExtensionServer/appsettings_hyperv.json +++ b/HyperVExtension/src/HyperVExtensionServer/appsettings_hyperv.json @@ -6,7 +6,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -14,7 +14,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\hyperv.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/extensions/CoreWidgetProvider/corewidgets_appsettings.json b/extensions/CoreWidgetProvider/corewidgets_appsettings.json index d598474f02..349755f1f9 100644 --- a/extensions/CoreWidgetProvider/corewidgets_appsettings.json +++ b/extensions/CoreWidgetProvider/corewidgets_appsettings.json @@ -6,7 +6,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -14,7 +14,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\corewidgets.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/src/appsettings.json b/src/appsettings.json index d118f3fcc9..b16cd33ca4 100644 --- a/src/appsettings.json +++ b/src/appsettings.json @@ -15,7 +15,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -23,7 +23,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\devhome.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } From 13fe40ee5b81bd436f819381f80300b2704a5f4b Mon Sep 17 00:00:00 2001 From: Felipe G Date: Tue, 9 Apr 2024 11:11:04 -0700 Subject: [PATCH 078/121] Changing Select to Invoke button action (#2555) Co-authored-by: Felipe da Conceicao Guimaraes --- .../DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs | 2 +- .../Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/Dashboard/DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs b/tools/Dashboard/DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs index 10237c3949..9af21e4ed2 100644 --- a/tools/Dashboard/DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs +++ b/tools/Dashboard/DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs @@ -47,7 +47,7 @@ public void RemoveFromSelection() public void Select() { - IsSelected = true; + Invoke(); } protected override string GetClassNameCore() diff --git a/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs index c559a4ee09..d1e012ffa8 100644 --- a/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs @@ -204,7 +204,7 @@ private void MarkSize(SelectableMenuFlyoutItem menuSizeItem) }; menuSizeItem.Icon = fontIcon; var peer = FrameworkElementAutomationPeer.FromElement(menuSizeItem) as SelectableMenuFlyoutItemAutomationPeer; - peer.Select(); + peer.AddToSelection(); } private void AddCustomizeToWidgetMenu(MenuFlyout widgetMenuFlyout, WidgetViewModel widgetViewModel) From 690127e073915825848974845e97391464897b46 Mon Sep 17 00:00:00 2001 From: sshilov7 <51001703+sshilov7@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:56:20 -0700 Subject: [PATCH 079/121] Fix DevSetupAgent logging and some other fixes. (#2566) * Fix DevSetupAgent logging and some other fixes. * Address review comments. --- HyperVExtension/src/DevSetupAgent/Logging.cs | 21 ++ .../src/DevSetupAgent/NativeMethods.txt | 6 + HyperVExtension/src/DevSetupAgent/Program.cs | 108 +++++++-- .../DevSetupAgent/Requests/ErrorRequest.cs | 7 +- .../DevSetupAgent/Requests/RequestFactory.cs | 1 - .../DevSetupAgent/Responses/ErrorResponse.cs | 15 +- .../ErrorUnsupportedRequestResponse.cs | 2 +- HyperVExtension/src/DevSetupEngine/Logging.cs | 8 +- .../HResultException.cs | 4 +- .../ApplyConfigurationOperation.cs | 1 + .../Helpers/DevSetupAgentDeploymentHelper.cs | 208 +----------------- .../HyperVExtension/HyperVExtension.csproj | 4 + .../HyperVExtension/Scripts/DevSetupAgent.ps1 | 208 ++++++++++++++++++ 13 files changed, 361 insertions(+), 232 deletions(-) create mode 100644 HyperVExtension/src/DevSetupAgent/Logging.cs rename HyperVExtension/src/{HyperVExtension/CommunicationWithGuest => HyperVExtension.HostGuestCommunication}/HResultException.cs (66%) create mode 100644 HyperVExtension/src/HyperVExtension/Scripts/DevSetupAgent.ps1 diff --git a/HyperVExtension/src/DevSetupAgent/Logging.cs b/HyperVExtension/src/DevSetupAgent/Logging.cs new file mode 100644 index 0000000000..920111d914 --- /dev/null +++ b/HyperVExtension/src/DevSetupAgent/Logging.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Windows.Storage; + +namespace HyperVExtension.DevSetupAgent; + +public class Logging +{ + public static readonly string LogExtension = ".dhlog"; + + public static readonly string LogFolderName = "Logs"; + + public static readonly string AppName = "DevSetupAgent"; + + public static readonly string DefaultLogFileName = "hyperv_setup"; + + private static readonly Lazy _logFolderRoot = new(() => Path.Combine(Path.GetTempPath(), AppName, LogFolderName)); + + public static readonly string LogFolderRoot = _logFolderRoot.Value; +} diff --git a/HyperVExtension/src/DevSetupAgent/NativeMethods.txt b/HyperVExtension/src/DevSetupAgent/NativeMethods.txt index 13b6a24b02..82f030d248 100644 --- a/HyperVExtension/src/DevSetupAgent/NativeMethods.txt +++ b/HyperVExtension/src/DevSetupAgent/NativeMethods.txt @@ -6,8 +6,14 @@ CLSCTX WIN32_ERROR S_OK E_FAIL +E_UNEXPECTED LsaEnumerateLogonSessions LsaGetLogonSessionData Windows.Win32.Security.Authentication.Identity.LsaFreeReturnBuffer SECURITY_LOGON_TYPE STATUS_SUCCESS +MakeAbsoluteSD +ConvertStringSecurityDescriptorToSecurityDescriptor +LocalAlloc +LocalFree +SDDL_REVISION_1 diff --git a/HyperVExtension/src/DevSetupAgent/Program.cs b/HyperVExtension/src/DevSetupAgent/Program.cs index d8dbf882e2..dbc372a24c 100644 --- a/HyperVExtension/src/DevSetupAgent/Program.cs +++ b/HyperVExtension/src/DevSetupAgent/Program.cs @@ -1,34 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.ComponentModel; using System.Runtime.InteropServices; using HyperVExtension.DevSetupAgent; +using HyperVExtension.HostGuestCommunication; using Serilog; using Windows.Win32; +using Windows.Win32.Foundation; using Windows.Win32.Security; using Windows.Win32.System.Com; -unsafe -{ - // TODO: Set real security descriptor to allow access from System+Admns+Interactive Users - var hr = PInvoke.CoInitializeSecurity( - new(null), - -1, - null, - null, - RPC_C_AUTHN_LEVEL.RPC_C_AUTHN_LEVEL_DEFAULT, - RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IDENTIFY, - null, - EOLE_AUTHENTICATION_CAPABILITIES.EOAC_NONE); - - if (hr < 0) - { - Marshal.ThrowExceptionForHR(hr); - } -} - // Set up Logging -Environment.SetEnvironmentVariable("DEVHOME_LOGS_ROOT", Path.Join(HyperVExtension.DevSetupEngine.Logging.LogFolderRoot, "HyperV")); +Environment.SetEnvironmentVariable("DEVHOME_LOGS_ROOT", Path.Join(Logging.LogFolderRoot, "HyperV")); var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings_hypervsetupagent.json") .Build(); @@ -36,6 +20,90 @@ .ReadFrom.Configuration(configuration) .CreateLogger(); +unsafe +{ + PSECURITY_DESCRIPTOR absolutSd = new(null); + PSID ownerSid = new(null); + PSID groupSid = new(null); + ACL* dacl = default; + ACL* sacl = default; + + try + { + // O:PSG:BU Owner Principal Self, Group Built-in Users + // (A;;0x3;;;SY) Allow Local System + // (A;;0x3;;;IU) Allow Interactive User + var accessPermission = "O:PSG:BUD:(A;;0x3;;;SY)(A;;0x3;;;IU)"; + uint securityDescriptorSize; + PInvoke.ConvertStringSecurityDescriptorToSecurityDescriptor(accessPermission, PInvoke.SDDL_REVISION_1, out var securityDescriptor, &securityDescriptorSize); + + uint absoluteSdSize = default; + uint daclSize = default; + uint saclSize = default; + uint ownerSize = default; + uint groupSize = default; + + if (PInvoke.MakeAbsoluteSD(securityDescriptor, absolutSd, ref absoluteSdSize, null, ref daclSize, null, ref saclSize, ownerSid, ref ownerSize, groupSid, ref groupSize)) + { + throw new HResultException(HRESULT.E_UNEXPECTED); + } + + var error = Marshal.GetLastWin32Error(); + if (error != (int)WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER) + { + throw new Win32Exception(error); + } + + absolutSd = new(PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, absoluteSdSize)); + dacl = (ACL*)PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, daclSize); + sacl = (ACL*)PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, saclSize); + ownerSid = new(PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, ownerSize)); + groupSid = new(PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, groupSize)); + + if (!PInvoke.MakeAbsoluteSD(securityDescriptor, absolutSd, ref absoluteSdSize, dacl, ref daclSize, sacl, ref saclSize, ownerSid, ref ownerSize, groupSid, ref groupSize)) + { + throw new HResultException(Marshal.GetLastWin32Error()); + } + + var hr = PInvoke.CoInitializeSecurity( + absolutSd, + -1, + null, + null, + RPC_C_AUTHN_LEVEL.RPC_C_AUTHN_LEVEL_DEFAULT, + RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IDENTIFY, + null, + EOLE_AUTHENTICATION_CAPABILITIES.EOAC_NONE); + + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + } + finally + { + if (sacl != default) + { + PInvoke.LocalFree((HLOCAL)sacl); + } + + if (dacl != default) + { + PInvoke.LocalFree((HLOCAL)dacl); + } + + if (groupSid != default) + { + PInvoke.LocalFree((HLOCAL)groupSid.Value); + } + + if (ownerSid != default) + { + PInvoke.LocalFree((HLOCAL)ownerSid.Value); + } + } +} + var host = Host.CreateDefaultBuilder(args) .UseWindowsService(options => { diff --git a/HyperVExtension/src/DevSetupAgent/Requests/ErrorRequest.cs b/HyperVExtension/src/DevSetupAgent/Requests/ErrorRequest.cs index fabc9a2052..725f3dedf5 100644 --- a/HyperVExtension/src/DevSetupAgent/Requests/ErrorRequest.cs +++ b/HyperVExtension/src/DevSetupAgent/Requests/ErrorRequest.cs @@ -9,10 +9,11 @@ namespace HyperVExtension.DevSetupAgent; /// internal class ErrorRequest : IHostRequest { - public ErrorRequest(IRequestMessage requestMessage) + public ErrorRequest(IRequestMessage requestMessage, Exception? ex = null) { Timestamp = DateTime.UtcNow; RequestId = requestMessage.RequestId!; + Error = ex; } public virtual uint Version { get; set; } = 1; @@ -27,6 +28,8 @@ public ErrorRequest(IRequestMessage requestMessage) public virtual IHostResponse Execute(ProgressHandler progressHandler, CancellationToken stoppingToken) { - return new ErrorResponse(RequestId); + return new ErrorResponse(RequestId, Error); } + + private Exception? Error { get; } } diff --git a/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs b/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs index 7067634443..2323e714a4 100644 --- a/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs +++ b/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs @@ -42,7 +42,6 @@ public IHostRequest CreateRequest(IRequestContext requestContext) { if (_requestFactories.TryGetValue(requestType, out var createRequest)) { - // TODO: Try/catch error. requestContext.JsonData = requestJson!; return createRequest(requestContext); } diff --git a/HyperVExtension/src/DevSetupAgent/Responses/ErrorResponse.cs b/HyperVExtension/src/DevSetupAgent/Responses/ErrorResponse.cs index 4428ebf76e..da53faf194 100644 --- a/HyperVExtension/src/DevSetupAgent/Responses/ErrorResponse.cs +++ b/HyperVExtension/src/DevSetupAgent/Responses/ErrorResponse.cs @@ -9,11 +9,20 @@ namespace HyperVExtension.DevSetupAgent; /// internal sealed class ErrorResponse : ResponseBase { - public ErrorResponse(string requestId) + public ErrorResponse(string requestId, Exception? error) : base(requestId) { - Status = Windows.Win32.Foundation.HRESULT.E_FAIL; - ErrorDescription = "Missing Request data."; + if (error != null) + { + ErrorDescription = error.Message; + Status = (uint)error.HResult; + } + else + { + ErrorDescription = "Missing Request data."; + Status = Windows.Win32.Foundation.HRESULT.E_FAIL; + } + GenerateJsonData(); } } diff --git a/HyperVExtension/src/DevSetupAgent/Responses/ErrorUnsupportedRequestResponse.cs b/HyperVExtension/src/DevSetupAgent/Responses/ErrorUnsupportedRequestResponse.cs index c0e0331f13..e57dfd4a9a 100644 --- a/HyperVExtension/src/DevSetupAgent/Responses/ErrorUnsupportedRequestResponse.cs +++ b/HyperVExtension/src/DevSetupAgent/Responses/ErrorUnsupportedRequestResponse.cs @@ -13,7 +13,7 @@ public ErrorUnsupportedRequestResponse(string requestId, string requestType) : base(requestId, requestType) { Status = Windows.Win32.Foundation.HRESULT.E_FAIL; - ErrorDescription = "Missing Request type."; + ErrorDescription = "Unsupported Request type."; GenerateJsonData(); } } diff --git a/HyperVExtension/src/DevSetupEngine/Logging.cs b/HyperVExtension/src/DevSetupEngine/Logging.cs index e3e5e40435..a2cdefcdfe 100644 --- a/HyperVExtension/src/DevSetupEngine/Logging.cs +++ b/HyperVExtension/src/DevSetupEngine/Logging.cs @@ -9,11 +9,13 @@ public class Logging { public static readonly string LogExtension = ".dhlog"; - public static readonly string LogFolderName = "Logs"; + public static readonly string LogFolderName = "Logs"; + + public static readonly string AppName = "DevSetupEngine"; public static readonly string DefaultLogFileName = "hyperv_setup"; - - private static readonly Lazy _logFolderRoot = new(() => Path.Combine(ApplicationData.Current.TemporaryFolder.Path, LogFolderName)); + + private static readonly Lazy _logFolderRoot = new(() => Path.Combine(Path.GetTempPath(), AppName, LogFolderName)); public static readonly string LogFolderRoot = _logFolderRoot.Value; } diff --git a/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/HResultException.cs b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HResultException.cs similarity index 66% rename from HyperVExtension/src/HyperVExtension/CommunicationWithGuest/HResultException.cs rename to HyperVExtension/src/HyperVExtension.HostGuestCommunication/HResultException.cs index f812115e95..c64c211b30 100644 --- a/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/HResultException.cs +++ b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HResultException.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace HyperVExtension.CommunicationWithGuest; +namespace HyperVExtension.HostGuestCommunication; -internal sealed class HResultException : Exception +public sealed class HResultException : Exception { public HResultException(int resultCode, string? description = null) : base(description) diff --git a/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/ApplyConfigurationOperation.cs b/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/ApplyConfigurationOperation.cs index 08c70efa37..5b8c3165ed 100644 --- a/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/ApplyConfigurationOperation.cs +++ b/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/ApplyConfigurationOperation.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using HyperVExtension.HostGuestCommunication; using HyperVExtension.Models; using Microsoft.Windows.DevHome.SDK; using Windows.Foundation; diff --git a/HyperVExtension/src/HyperVExtension/Helpers/DevSetupAgentDeploymentHelper.cs b/HyperVExtension/src/HyperVExtension/Helpers/DevSetupAgentDeploymentHelper.cs index cb6ccaeaf6..b5796b7cc1 100644 --- a/HyperVExtension/src/HyperVExtension/Helpers/DevSetupAgentDeploymentHelper.cs +++ b/HyperVExtension/src/HyperVExtension/Helpers/DevSetupAgentDeploymentHelper.cs @@ -3,6 +3,7 @@ using System.Management.Automation; using System.Security; +using System.Text; using HyperVExtension.Exceptions; using HyperVExtension.Services; @@ -28,6 +29,7 @@ private enum ProcessorArchitecture : ushort private readonly IPowerShellService _powerShellService; private readonly string _vmId; + private readonly Lazy _script = new(() => LoadScript()); public DevSetupAgentDeploymentHelper(IPowerShellService powerShellService, string vmId) { @@ -43,7 +45,7 @@ public void DeployDevSetupAgent(string userName, SecureString password) var sourcePath = GetSourcePath(architecture); var deployDevSetupAgentStatement = new StatementBuilder() - .AddScript(_script, false) + .AddScript(_script.Value, false) .AddCommand("Install-DevSetupAgent") .AddParameter("VMId", _vmId) .AddParameter("Session", session) @@ -124,203 +126,9 @@ private ushort GetVMArchitechture(PSObject session) return (ushort)psObject.BaseObject; } - private readonly string _script = @" -function Install-DevSetupAgent -{ - Param( - [Parameter(Mandatory = $true)] - [Guid] $VMId, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.Runspaces.PSSession] $Session, - - [Parameter(Mandatory = $true)] - [string] $Path - ) - - $ErrorActionPreference = ""Stop"" - $activity = ""Installing DevSetupAgent to VM $VMId"" - - # Validate input. Only .cab and .zip files are supported - # If $Path is a directory, it will be copied to the VM and installed as is - $isDirectory = $false - $isCab = $false - $inputFileName = $null - if (Test-Path -Path $Path -PathType 'Container') - { - $isDirectory = $true - } - elseif (Test-Path -Path $Path -PathType 'Leaf') - { - if ($Path -match '\.(cab)$') - { - $isCab = $true - } - elseif (-not $Path -match '\.(zip)$') - { - throw ""Only .cab and .zip files are supported"" - } - $inputFileName = Split-Path -Path $Path -Leaf - } - else - { - throw ""$Path does not exist"" - } - - - $DevSetupAgentConst = ""DevSetupAgent"" - $DevSetupEngineConst = ""DevSetupEngine"" - $session = $Session - - $guestTempDirectory = Invoke-Command -Session $session -ScriptBlock { $env:temp } - - [string] $guid = [System.Guid]::NewGuid() - $guestUnpackDirectory = Join-Path -Path $guestTempDirectory -ChildPath $guid - $guestDevSetupAgentTempDirectory = Join-Path -Path $guestUnpackDirectory -ChildPath $DevSetupAgentConst - - Write-Host ""Creating VM temporary folder $guestUnpackDirectory"" - Write-Progress -Activity $activity -Status ""Creating VM temporary folder $guestUnpackDirectory"" -PercentComplete 10 - Invoke-Command -Session $session -ScriptBlock { New-Item -Path ""$using:guestUnpackDirectory"" -ItemType ""directory"" } - - if ($isDirectory) - { - $destinationPath = $guestDevSetupAgentTempDirectory - } - else - { - $destinationPath = $guestUnpackDirectory - } - - Write-Host ""Copying $Path to VM $destinationPath"" - Write-Progress -Activity $activity -Status ""Copying DevSetupAgent to VM $destinationPath"" -PercentComplete 15 - Copy-Item -ToSession $session -Recurse -Path $Path -Destination $destinationPath - - - Invoke-Command -Session $session -ScriptBlock { - $ErrorActionPreference = ""Stop"" - - try - { - $guestDevSetupAgentPath = Join-Path -Path $Env:Programfiles -ChildPath $using:DevSetupAgentConst - - # Stop and remove previous version of DevSetupAgent service if it exists - $service = Get-Service -Name $using:DevSetupAgentConst -ErrorAction SilentlyContinue - if ($service) - { - $serviceWMI = Get-WmiObject -Class Win32_Service -Filter ""Name='$using:DevSetupAgentConst'"" - $existingServicePath = $serviceWMI.Properties[""PathName""].Value - if ($existingServicePath) - { - $guestDevSetupAgentPath = Split-Path $existingServicePath -Parent - } - - try - { - Write-Host ""Stopping DevSetupAgent service"" - Write-Progress -Activity $using:activity -Status ""Stopping DevSetupAgent service $destinationPath"" -PercentComplete 30 - $service.Stop() - } - catch - { - Write-Host ""Ignoring error: $PSItem"" - } - - Remove-Variable -Name service -ErrorAction SilentlyContinue - - # Remove-Service is only available in PowerShell 6.0 and later. Windows doesn't come with it preinstalled. - Write-Host ""Removing DevSetupAgent service"" - Write-Progress -Activity $using:activity -Status ""Removing DevSetupAgent service"" -PercentComplete 35 - $serviceWMI = Get-WmiObject -Class Win32_Service -Filter ""Name='$using:DevSetupAgentConst'"" - $serviceWMI.Delete() - Remove-Variable -Name serviceWMI -ErrorAction SilentlyContinue - } - - # Stop previous version of DevSetupEngine COM server if it exists - $devSetupEngineProcess = Get-Process -Name ""$using:DevSetupEngineConst"" -ErrorAction SilentlyContinue - if ($devSetupEngineProcess -ne $null) - { - Write-Host ""Stopping $using:DevSetupEngineConst process"" - Write-Progress -Activity $using:activity -Status ""Stopping $using:DevSetupEngineConst process"" -PercentComplete 40 - Stop-Process -Force -Name ""$using:DevSetupEngineConst"" - } - - # Unregister DevSetupEngine - $enginePath = Join-Path -Path $guestDevSetupAgentPath -ChildPath ""$using:DevSetupEngineConst.exe"" - if (Test-Path -Path $enginePath) - { - Write-Host ""Unregistering DevSetupEngine ($enginePath)"" - Write-Progress -Activity $using:activity -Status ""Registering DevSetupEngine ($enginePath)"" -PercentComplete 88 - &$enginePath ""-UnregisterComServer"" - } - - # Remove previous version of DevSetupAgent service files - if (Test-Path -Path $guestDevSetupAgentPath) - { - # Sleep a few seconds to make sure all handles released after shutting down previous DevSetupEngine - Start-Sleep -Seconds 7 - Write-Host ""Deleting old DevSetupAgent service files"" - Write-Progress -Activity $using:activity -Status ""Deleting old DevSetupAgent service files"" -PercentComplete 45 - Remove-Item -Recurse -Force -Path $guestDevSetupAgentPath - } - - if ($using:isDirectory) - { - Write-Host ""Copying DevSetupAgent to $guestDevSetupAgentPath"" - Write-Progress -Activity $using:activity -Status ""Deleting old DevSetupAgent service files"" -PercentComplete 50 - Copy-Item -Recurse -Path $using:guestDevSetupAgentTempDirectory -Destination $guestDevSetupAgentPath - } - elseif ($using:isCab) - { - $cabPath = Join-Path -Path $using:guestUnpackDirectory -ChildPath $using:inputFileName - Write-Host ""Unpacking $cabPath to $guestDevSetupAgentPath"" - Write-Progress -Activity $using:activity -Status ""Unpacking $cabPath to $guestDevSetupAgentPath"" -PercentComplete 60 - $expandOutput=&""$Env:SystemRoot\System32\expand.exe"" $cabPath /F:* $Env:Programfiles - if ($LastExitCode -ne 0) - { - throw ""Error unpacking $cabPath`:`n$LastExitCode`n$($expandOutput|Out-String)"" - } - } - else - { - $zipPath = Join-Path -Path $using:guestUnpackDirectory -ChildPath $using:inputFileName - Write-Host ""Unpacking $using:inputFileName to $guestDevSetupAgentPath"" - Write-Progress -Activity $using:activity -Status ""Unpacking $using:inputFileName to $guestDevSetupAgentPath"" -PercentComplete 60 - Expand-Archive -Path $zipPath -Destination $guestDevSetupAgentPath - } - - # Register DevSetupAgent service - $servicePath = Join-Path -Path $guestDevSetupAgentPath -ChildPath ""$using:DevSetupAgentConst.exe"" - Write-Host ""Registering DevSetupAgent service ($servicePath)"" - Write-Progress -Activity $using:activity -Status ""Registering DevSetupAgent service ($servicePath)"" -PercentComplete 85 - New-Service -Name $using:DevSetupAgentConst -BinaryPathName $servicePath -StartupType Automatic - - # Register DevSetupEngine - Write-Host ""Registering DevSetupEngine ($enginePath)"" - Write-Progress -Activity $using:activity -Status ""Registering DevSetupEngine ($enginePath)"" -PercentComplete 88 - - # Executing non-console apps using '&' does not set $LastExitCode. Using Start-Process here to get the returned error code. - $process = Start-Process -NoNewWindow -Wait $enginePath -ArgumentList ""-RegisterComServer"" -PassThru - if ($process.ExitCode -ne 0) - { - throw ""Error registering $enginePath`: $process.ExitCode"" - } - - Write-Host ""Starting DevSetupAgent service"" - Write-Progress -Activity $using:activity -Status ""Starting DevSetupAgent service"" -PercentComplete 92 - Start-Service $using:DevSetupAgentConst - } - catch - { - Write-Host ""Error on guest OS: $PSItem"" - } - finally - { - Write-Host ""Removing temporary directory $using:guestUnpackDirectory"" - Remove-Item -Recurse -Force -Path $using:guestUnpackDirectory -ErrorAction SilentlyContinue - } - } - - Remove-PSSession $session -} -"; + private static string LoadScript() + { + var path = Path.Combine(AppContext.BaseDirectory, "HyperVExtension", "Scripts", "DevSetupAgent.ps1"); + return File.ReadAllText(path, Encoding.Default) ?? throw new FileNotFoundException(path); + } } diff --git a/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj b/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj index 2e6b76865e..4e3f564cf0 100644 --- a/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj +++ b/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj @@ -8,10 +8,14 @@ win-x86;win-x64;win-arm64 + + + Always + Always diff --git a/HyperVExtension/src/HyperVExtension/Scripts/DevSetupAgent.ps1 b/HyperVExtension/src/HyperVExtension/Scripts/DevSetupAgent.ps1 new file mode 100644 index 0000000000..f81bce1a5d --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Scripts/DevSetupAgent.ps1 @@ -0,0 +1,208 @@ +<# +.SYNOPSIS + +Install DevSetupAgent Windows service and DevSetupEngine COM server on a VM + +.DESCRIPTION + +Install DevSetupAgent Windows service and DevSetupEngine COM server on a VM +through the provided PSSession. +#> + +function Install-DevSetupAgent +{ + Param( + [Parameter(Mandatory = $true)] + [Guid] $VMId, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.Runspaces.PSSession] $Session, + + [Parameter(Mandatory = $true)] + [string] $Path + ) + + $ErrorActionPreference = "Stop" + $activity = "Installing DevSetupAgent to VM $VMId" + + # Validate input. Only .cab and .zip files are supported + # If $Path is a directory, it will be copied to the VM and installed as is + $isDirectory = $false + $isCab = $false + $inputFileName = $null + if (Test-Path -Path $Path -PathType 'Container') + { + $isDirectory = $true + } + elseif (Test-Path -Path $Path -PathType 'Leaf') + { + if ($Path -match '\.(cab)$') + { + $isCab = $true + } + elseif (-not $Path -match '\.(zip)$') + { + throw "Only .cab and .zip files are supported" + } + $inputFileName = Split-Path -Path $Path -Leaf + } + else + { + throw "$Path does not exist" + } + + + $DevSetupAgentConst = "DevSetupAgent" + $DevSetupEngineConst = "DevSetupEngine" + $session = $Session + + $guestTempDirectory = Invoke-Command -Session $session -ScriptBlock { $env:temp } + + [string] $guid = [System.Guid]::NewGuid() + $guestUnpackDirectory = Join-Path -Path $guestTempDirectory -ChildPath $guid + $guestDevSetupAgentTempDirectory = Join-Path -Path $guestUnpackDirectory -ChildPath $DevSetupAgentConst + + Write-Host "Creating VM temporary folder $guestUnpackDirectory" + Write-Progress -Activity $activity -Status "Creating VM temporary folder $guestUnpackDirectory" -PercentComplete 10 + Invoke-Command -Session $session -ScriptBlock { New-Item -Path "$using:guestUnpackDirectory" -ItemType "directory" } + + if ($isDirectory) + { + $destinationPath = $guestDevSetupAgentTempDirectory + } + else + { + $destinationPath = $guestUnpackDirectory + } + + Write-Host "Copying $Path to VM $destinationPath" + Write-Progress -Activity $activity -Status "Copying DevSetupAgent to VM $destinationPath" -PercentComplete 15 + Copy-Item -ToSession $session -Recurse -Path $Path -Destination $destinationPath + + + Invoke-Command -Session $session -ScriptBlock { + $ErrorActionPreference = "Stop" + + try + { + $guestDevSetupAgentPath = Join-Path -Path $Env:Programfiles -ChildPath $using:DevSetupAgentConst + + # Stop and remove previous version of DevSetupAgent service if it exists + $service = Get-Service -Name $using:DevSetupAgentConst -ErrorAction SilentlyContinue + if ($service) + { + $serviceWMI = Get-WmiObject -Class Win32_Service -Filter "Name='$using:DevSetupAgentConst'" + $existingServicePath = $serviceWMI.Properties["PathName"].Value + if ($existingServicePath) + { + $guestDevSetupAgentPath = Split-Path $existingServicePath -Parent + } + + try + { + Write-Host "Stopping DevSetupAgent service" + Write-Progress -Activity $using:activity -Status "Stopping DevSetupAgent service $destinationPath" -PercentComplete 30 + $service.Stop() + } + catch + { + Write-Host "Ignoring error: $PSItem" + } + + Remove-Variable -Name service -ErrorAction SilentlyContinue + + # Remove-Service is only available in PowerShell 6.0 and later. Windows doesn't come with it preinstalled. + Write-Host "Removing DevSetupAgent service" + Write-Progress -Activity $using:activity -Status "Removing DevSetupAgent service" -PercentComplete 35 + $serviceWMI = Get-WmiObject -Class Win32_Service -Filter "Name='$using:DevSetupAgentConst'" + $serviceWMI.Delete() + Remove-Variable -Name serviceWMI -ErrorAction SilentlyContinue + } + + # Stop previous version of DevSetupEngine COM server if it exists + $devSetupEngineProcess = Get-Process -Name "$using:DevSetupEngineConst" -ErrorAction SilentlyContinue + if ($devSetupEngineProcess -ne $null) + { + Write-Host "Stopping $using:DevSetupEngineConst process" + Write-Progress -Activity $using:activity -Status "Stopping $using:DevSetupEngineConst process" -PercentComplete 40 + Stop-Process -Force -Name "$using:DevSetupEngineConst" + } + + # Unregister DevSetupEngine + $enginePath = Join-Path -Path $guestDevSetupAgentPath -ChildPath "$using:DevSetupEngineConst.exe" + if (Test-Path -Path $enginePath) + { + Write-Host "Unregistering DevSetupEngine ($enginePath)" + Write-Progress -Activity $using:activity -Status "Registering DevSetupEngine ($enginePath)" -PercentComplete 88 + &$enginePath "-UnregisterComServer" + } + + # Remove previous version of DevSetupAgent service files + if (Test-Path -Path $guestDevSetupAgentPath) + { + # Sleep a few seconds to make sure all handles released after shutting down previous DevSetupEngine + Start-Sleep -Seconds 7 + Write-Host "Deleting old DevSetupAgent service files" + Write-Progress -Activity $using:activity -Status "Deleting old DevSetupAgent service files" -PercentComplete 45 + Remove-Item -Recurse -Force -Path $guestDevSetupAgentPath + } + + if ($using:isDirectory) + { + Write-Host "Copying DevSetupAgent to $guestDevSetupAgentPath" + Write-Progress -Activity $using:activity -Status "Deleting old DevSetupAgent service files" -PercentComplete 50 + Copy-Item -Recurse -Path $using:guestDevSetupAgentTempDirectory -Destination $guestDevSetupAgentPath + } + elseif ($using:isCab) + { + $cabPath = Join-Path -Path $using:guestUnpackDirectory -ChildPath $using:inputFileName + Write-Host "Unpacking $cabPath to $guestDevSetupAgentPath" + Write-Progress -Activity $using:activity -Status "Unpacking $cabPath to $guestDevSetupAgentPath" -PercentComplete 60 + $expandOutput=&"$Env:SystemRoot\System32\expand.exe" $cabPath /F:* $Env:Programfiles + if ($LastExitCode -ne 0) + { + throw "Error unpacking $cabPath`:`n$LastExitCode`n$($expandOutput|Out-String)" + } + } + else + { + $zipPath = Join-Path -Path $using:guestUnpackDirectory -ChildPath $using:inputFileName + Write-Host "Unpacking $using:inputFileName to $guestDevSetupAgentPath" + Write-Progress -Activity $using:activity -Status "Unpacking $using:inputFileName to $guestDevSetupAgentPath" -PercentComplete 60 + Expand-Archive -Path $zipPath -Destination $guestDevSetupAgentPath + } + + # Register DevSetupAgent service + $servicePath = Join-Path -Path $guestDevSetupAgentPath -ChildPath "$using:DevSetupAgentConst.exe" + Write-Host "Registering DevSetupAgent service ($servicePath)" + Write-Progress -Activity $using:activity -Status "Registering DevSetupAgent service ($servicePath)" -PercentComplete 85 + New-Service -Name $using:DevSetupAgentConst -BinaryPathName $servicePath -StartupType Automatic + + # Register DevSetupEngine + Write-Host "Registering DevSetupEngine ($enginePath)" + Write-Progress -Activity $using:activity -Status "Registering DevSetupEngine ($enginePath)" -PercentComplete 88 + + # Executing non-console apps using '&' does not set $LastExitCode. Using Start-Process here to get the returned error code. + $process = Start-Process -NoNewWindow -Wait $enginePath -ArgumentList "-RegisterComServer" -PassThru + if ($process.ExitCode -ne 0) + { + throw "Error registering $enginePath`: $process.ExitCode" + } + + Write-Host "Starting DevSetupAgent service" + Write-Progress -Activity $using:activity -Status "Starting DevSetupAgent service" -PercentComplete 92 + Start-Service $using:DevSetupAgentConst + } + catch + { + Write-Host "Error on guest OS: $PSItem" + } + finally + { + Write-Host "Removing temporary directory $using:guestUnpackDirectory" + Remove-Item -Recurse -Force -Path $using:guestUnpackDirectory -ErrorAction SilentlyContinue + } + } + + Remove-PSSession $session +} From b747810dd3f9f5294cb57196e957a58d2aec7918 Mon Sep 17 00:00:00 2001 From: Eric Johnson Date: Wed, 10 Apr 2024 10:03:19 -0700 Subject: [PATCH 080/121] Correctly show/hide experimental features in stable builds (#2572) --- docs/ExperimentalFeatures.md | 2 +- src/NavConfig.jsonc | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/ExperimentalFeatures.md b/docs/ExperimentalFeatures.md index 3bf567276e..3b42f557f7 100644 --- a/docs/ExperimentalFeatures.md +++ b/docs/ExperimentalFeatures.md @@ -24,7 +24,7 @@ This is useful for features that are not ready for general use, but can be teste "visible": true }, { - "buildType": "release", + "buildType": "stable", "enabledByDefault": false, "visible": false } diff --git a/src/NavConfig.jsonc b/src/NavConfig.jsonc index fb191dc10a..c65878b80c 100644 --- a/src/NavConfig.jsonc +++ b/src/NavConfig.jsonc @@ -45,7 +45,7 @@ { "buildType": "dev", "enabledByDefault": true, - "visible": true + "visible": false }, { "buildType": "canary", @@ -53,7 +53,7 @@ "visible": true }, { - "buildType": "release", + "buildType": "stable", "enabledByDefault": false, "visible": false } @@ -74,7 +74,7 @@ "visible": false }, { - "buildType": "release", + "buildType": "stable", "enabledByDefault": false, "visible": false } @@ -95,7 +95,7 @@ "visible": false }, { - "buildType": "release", + "buildType": "stable", "enabledByDefault": false, "visible": false } @@ -116,7 +116,7 @@ "visible": true }, { - "buildType": "release", + "buildType": "stable", "enabledByDefault": false, "visible": false } From 05ddcd86ab70b552b0061699f61bbf6bc8ede244 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Wed, 10 Apr 2024 12:34:22 -0700 Subject: [PATCH 081/121] Using Content in SecondaryWindow (#2571) * Removing the first item from tab stop * Removing an un-used using * Adding comments * Using content instead of WindowContent * Reverting DevDriveView. --------- Co-authored-by: Darren Hoehna --- common/Windows/SecondaryWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/Windows/SecondaryWindow.cs b/common/Windows/SecondaryWindow.cs index 32794dc0dc..845dbddfec 100644 --- a/common/Windows/SecondaryWindow.cs +++ b/common/Windows/SecondaryWindow.cs @@ -184,7 +184,7 @@ public SecondaryWindow() // Initialize window content template _windowTemplate = new(this); - WindowContent = _windowTemplate; + Content = _windowTemplate; // Register secondary window events handlers Activated += OnSecondaryWindowActivated; From 79ed0a94276acf7d7fc5ab49679b4e613dd362a6 Mon Sep 17 00:00:00 2001 From: David Bennett Date: Wed, 10 Apr 2024 12:36:10 -0700 Subject: [PATCH 082/121] Fix exception logging (#2578) --- .../src/DevSetupAgent/DevAgentService.cs | 4 +-- .../src/DevSetupAgent/HostRegistryChannel.cs | 8 ++--- .../src/DevSetupAgent/RegistryWatcher.cs | 4 +-- .../src/DevSetupAgent/RequestManager.cs | 2 +- .../DevSetupAgent/Requests/RequestFactory.cs | 2 +- .../DevSetupEngine/ConfigurationFileHelper.cs | 9 +++-- .../PackageOperationException.cs | 2 +- HyperVExtension/src/DevSetupEngine/Program.cs | 2 +- .../Providers/MessageHelper.cs | 2 +- .../Responses/ResponseFactory.cs | 2 +- .../HyperVExtension/Helpers/PsObjectHelper.cs | 2 +- .../src/HyperVExtension/Helpers/Resources.cs | 2 +- .../src/HyperVExtension/HyperVExtension.cs | 2 +- .../Models/HyperVVirtualMachine.cs | 28 +++++++-------- .../VMGalleryVMCreationOperation.cs | 8 ++--- .../Models/VmCredentialAdaptiveCardSession.cs | 2 +- .../Models/WaitForLoginAdaptiveCardSession.cs | 2 +- .../Providers/HyperVProvider.cs | 4 +-- .../Services/PowerShellService.cs | 2 +- .../Services/VMGalleryService.cs | 2 +- .../Helpers/ComputeSystemHelpers.cs | 4 +-- .../Helpers/StringResourceHelper.cs | 2 +- common/Environments/Models/CardProperty.cs | 4 +-- common/Environments/Models/ComputeSystem.cs | 36 +++++++++---------- .../Models/ComputeSystemProvider.cs | 8 ++--- .../Models/CreateComputeSystemOperation.cs | 6 ++-- .../Services/ComputeSystemManager.cs | 6 ++-- common/Helpers/AdaptiveCardHelpers.cs | 2 +- common/Helpers/Deployment.cs | 2 +- common/Models/ExtensionAdaptiveCardSession.cs | 8 ++--- .../Services/AdaptiveCardRenderingService.cs | 2 +- common/Services/AppInstallManagerService.cs | 2 +- common/Services/ComputeSystemService.cs | 2 +- common/Services/NotificationService.cs | 12 +++---- .../CoreWidgetProvider/Helpers/GPUStats.cs | 2 +- .../Helpers/NetworkStats.cs | 2 +- .../CoreWidgetProvider/Helpers/Resources.cs | 2 +- .../CoreWidgetProvider/Widgets/CoreWidget.cs | 2 +- .../Widgets/SSHWalletWidget.cs | 4 +-- .../Widgets/SystemCPUUsageWidget.cs | 2 +- .../Widgets/SystemGPUUsageWidget.cs | 2 +- .../Widgets/SystemMemoryWidget.cs | 2 +- .../Widgets/SystemNetworkUsageWidget.cs | 2 +- .../Widgets/WidgetProvider.cs | 2 +- .../DevHome.Settings/Views/AboutPage.xaml.cs | 2 +- .../Views/AccountsPage.xaml.cs | 4 +-- src/Services/AccountsService.cs | 2 +- src/Services/DSCFileActivationHandler.cs | 2 +- src/ViewModels/InitializationViewModel.cs | 4 +-- .../OptimizeDevDriveDialogViewModel.cs | 4 +-- .../ViewModels/DevDriveInsightsViewModel.cs | 6 ++-- .../QuietBackgroundProcessesViewModel.cs | 8 ++--- .../Controls/WidgetControl.xaml.cs | 2 +- .../WidgetAdaptiveCardRenderingService.cs | 2 +- .../Services/WidgetHostingService.cs | 4 +-- .../ViewModels/WidgetViewModel.cs | 4 +-- .../Views/DashboardView.xaml.cs | 12 +++---- .../ViewModels/LandingPageViewModel.cs | 4 +-- .../ViewModels/ExtensionLibraryViewModel.cs | 2 +- .../DevDriveFormatter/DevDriveFormatter.cs | 2 +- .../Elevation/IPCSetup.cs | 4 +-- .../ElevatedComponentOperation.cs | 4 +-- .../Tasks/ElevatedConfigurationTask.cs | 2 +- .../Tasks/ElevatedInstallTask.cs | 2 +- .../DevHome.SetupFlow/Models/CloneRepoTask.cs | 4 +-- .../Models/CloningInformation.cs | 4 +-- .../Models/ConfigureTargetTask.cs | 8 ++--- .../DevHome.SetupFlow/Models/ConfigureTask.cs | 2 +- .../Models/CreateDevDriveTask.cs | 2 +- .../Models/GenericRepository.cs | 8 ++--- .../Models/InstallPackageTask.cs | 4 +-- .../Models/RepositoryProvider.cs | 18 +++++----- .../DevHome.SetupFlow/Models/WinGetPackage.cs | 2 +- .../SDKOpenConfigurationSetResult.cs | 2 +- .../Services/AppManagementInitializer.cs | 2 +- .../Services/CatalogDataSourceLoader.cs | 4 +-- .../Services/ComputeSystemViewModelFactory.cs | 2 +- .../Services/ConfigurationFileBuilder.cs | 2 +- .../Services/DevDriveManager.cs | 8 ++--- .../Services/WinGet/WinGetCatalogConnector.cs | 8 ++--- .../Services/WinGet/WinGetDeployment.cs | 12 +++---- .../Services/WinGet/WinGetRecovery.cs | 4 +-- .../WinGetFeaturedApplicationsDataSource.cs | 6 ++-- .../Services/WinGetPackageJsonDataSource.cs | 4 +-- .../WinGetPackageRestoreDataSource.cs | 4 +-- .../Utilities/DevDriveUtil.cs | 2 +- .../ViewModels/AddRepoViewModel.cs | 6 ++-- .../ViewModels/ConfigurationFileViewModel.cs | 10 +++--- .../ViewModels/DevDriveViewModel.cs | 4 +-- .../ComputeSystemsListViewModel.cs | 2 +- .../EnvironmentCreationOptionsViewModel.cs | 2 +- .../ViewModels/FolderPickerViewModel.cs | 2 +- .../ViewModels/PackageCatalogListViewModel.cs | 2 +- .../ViewModels/ReviewViewModel.cs | 4 +-- .../ViewModels/SearchViewModel.cs | 2 +- .../ViewModels/SetupTargetViewModel.cs | 4 +-- .../CreateEnvironmentReviewView.xaml.cs | 2 +- .../EnvironmentCreationOptionsView.xaml.cs | 2 +- 98 files changed, 217 insertions(+), 222 deletions(-) diff --git a/HyperVExtension/src/DevSetupAgent/DevAgentService.cs b/HyperVExtension/src/DevSetupAgent/DevAgentService.cs index f0b5c958c8..9b5b89a18b 100644 --- a/HyperVExtension/src/DevSetupAgent/DevAgentService.cs +++ b/HyperVExtension/src/DevSetupAgent/DevAgentService.cs @@ -41,13 +41,13 @@ protected async override Task ExecuteAsync(CancellationToken stoppingToken) } catch (Exception ex) { - _log.Error($"Exception in DevAgentService.", ex); + _log.Error(ex, $"Exception in DevAgentService."); } } } catch (Exception ex) { - _log.Error($"Failed to run DevSetupAgent.", ex); + _log.Error(ex, $"Failed to run DevSetupAgent."); throw; } finally diff --git a/HyperVExtension/src/DevSetupAgent/HostRegistryChannel.cs b/HyperVExtension/src/DevSetupAgent/HostRegistryChannel.cs index 518dc93ed2..11e8f8a076 100644 --- a/HyperVExtension/src/DevSetupAgent/HostRegistryChannel.cs +++ b/HyperVExtension/src/DevSetupAgent/HostRegistryChannel.cs @@ -113,7 +113,7 @@ await Task.Run( } catch (Exception ex) { - _log.Error($"Could not write host message. Response ID: {responseMessage.ResponseId}", ex); + _log.Error(ex, $"Could not write host message. Response ID: {responseMessage.ResponseId}"); } }, stoppingToken); @@ -130,7 +130,7 @@ await Task.Run( } catch (Exception ex) { - _log.Error($"Could not delete host message. Response ID: {responseId}", ex); + _log.Error(ex, $"Could not delete host message. Response ID: {responseId}"); } }, stoppingToken); @@ -207,7 +207,7 @@ private RequestMessage TryReadMessage() } catch (Exception ex) { - _log.Error($"Could not read host message {valueName}", ex); + _log.Error(ex, $"Could not read host message {valueName}"); } MessageHelper.DeleteAllMessages(_registryHiveKey, _fromHostRegistryKeyPath, s[0]); @@ -218,7 +218,7 @@ private RequestMessage TryReadMessage() } catch (Exception ex) { - _log.Error("Could not read host message.", ex); + _log.Error(ex, "Could not read host message."); } return requestMessage; diff --git a/HyperVExtension/src/DevSetupAgent/RegistryWatcher.cs b/HyperVExtension/src/DevSetupAgent/RegistryWatcher.cs index 019407c889..023f46ca27 100644 --- a/HyperVExtension/src/DevSetupAgent/RegistryWatcher.cs +++ b/HyperVExtension/src/DevSetupAgent/RegistryWatcher.cs @@ -76,14 +76,14 @@ public void Start() } catch (Exception ex) { - _log.Error("RegistryChanged delegate failed.", ex); + _log.Error(ex, "RegistryChanged delegate failed."); } } } } catch (Exception ex) { - _log.Error("Registry Watcher thread failed.", ex); + _log.Error(ex, "Registry Watcher thread failed."); } }); _log.Information("Registry Watcher thread started."); diff --git a/HyperVExtension/src/DevSetupAgent/RequestManager.cs b/HyperVExtension/src/DevSetupAgent/RequestManager.cs index 9c799d77f6..07cde9cc46 100644 --- a/HyperVExtension/src/DevSetupAgent/RequestManager.cs +++ b/HyperVExtension/src/DevSetupAgent/RequestManager.cs @@ -102,7 +102,7 @@ private void ProcessRequestQueue(CancellationToken stoppingToken) } catch (Exception ex) { - _log.Error($"Failed to execute request.", ex); + _log.Error(ex, $"Failed to execute request."); } } } diff --git a/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs b/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs index 2323e714a4..bd15df8917 100644 --- a/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs +++ b/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs @@ -64,7 +64,7 @@ public IHostRequest CreateRequest(IRequestContext requestContext) { var messageId = requestContext.RequestMessage.RequestId ?? ""; var requestData = requestContext.RequestMessage.RequestData ?? ""; - _log.Error($"Error processing message. Message ID: {messageId}. Request data: {requestData}", ex); + _log.Error(ex, $"Error processing message. Message ID: {messageId}. Request data: {requestData}"); return new ErrorRequest(requestContext.RequestMessage); } } diff --git a/HyperVExtension/src/DevSetupEngine/ConfigurationFileHelper.cs b/HyperVExtension/src/DevSetupEngine/ConfigurationFileHelper.cs index c7d7862dd7..c8885f2d4c 100644 --- a/HyperVExtension/src/DevSetupEngine/ConfigurationFileHelper.cs +++ b/HyperVExtension/src/DevSetupEngine/ConfigurationFileHelper.cs @@ -279,22 +279,21 @@ private void LogConfigurationDiagnostics(WinGet.IDiagnosticInformation diagnosti { _log.Information($"WinGet: {diagnosticInformation.Message}"); - var sourceComponent = nameof(WinGet.ConfigurationProcessor); switch (diagnosticInformation.Level) { case WinGet.DiagnosticLevel.Warning: - _log.Warning(sourceComponent, diagnosticInformation.Message); + _log.Warning(diagnosticInformation.Message); return; case WinGet.DiagnosticLevel.Error: - _log.Error(sourceComponent, diagnosticInformation.Message); + _log.Error(diagnosticInformation.Message); return; case WinGet.DiagnosticLevel.Critical: - _log.Fatal(sourceComponent, diagnosticInformation.Message); + _log.Fatal(diagnosticInformation.Message); return; case WinGet.DiagnosticLevel.Verbose: case WinGet.DiagnosticLevel.Informational: default: - _log.Information(sourceComponent, diagnosticInformation.Message); + _log.Information(diagnosticInformation.Message); return; } } diff --git a/HyperVExtension/src/DevSetupEngine/PackageOperationException.cs b/HyperVExtension/src/DevSetupEngine/PackageOperationException.cs index 9b149bede0..eaca912ed9 100644 --- a/HyperVExtension/src/DevSetupEngine/PackageOperationException.cs +++ b/HyperVExtension/src/DevSetupEngine/PackageOperationException.cs @@ -19,6 +19,6 @@ public PackageOperationException(ErrorCode errorCode, string message) { HResult = (int)errorCode; var log = Log.ForContext("SourceContext", nameof(PackageOperationException)); - log.Error(message, this); + log.Error(this, message); } } diff --git a/HyperVExtension/src/DevSetupEngine/Program.cs b/HyperVExtension/src/DevSetupEngine/Program.cs index 6bff3af2d6..4c1c269ebf 100644 --- a/HyperVExtension/src/DevSetupEngine/Program.cs +++ b/HyperVExtension/src/DevSetupEngine/Program.cs @@ -58,7 +58,7 @@ public static int Main([System.Runtime.InteropServices.WindowsRuntime.ReadOnlyAr } catch (Exception ex) { - Log.Error($"Exception: {ex}", ex); + Log.Error(ex, $"Exception: {ex}"); Log.CloseAndFlush(); return ex.HResult; } diff --git a/HyperVExtension/src/HyperVExtension.HostGuestCommunication/Providers/MessageHelper.cs b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/Providers/MessageHelper.cs index 8ec2c1ee14..6d8401907c 100644 --- a/HyperVExtension/src/HyperVExtension.HostGuestCommunication/Providers/MessageHelper.cs +++ b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/Providers/MessageHelper.cs @@ -110,7 +110,7 @@ public static Dictionary MergeMessageParts(Dictionary"; var responseData = message?.ResponseData ?? ""; - _log.Error($"Error processing message. Message ID: {messageId}. Request data: {responseData}", ex); + _log.Error(ex, $"Error processing message. Message ID: {messageId}. Request data: {responseData}"); return new ErrorResponse(message!); } } diff --git a/HyperVExtension/src/HyperVExtension/Helpers/PsObjectHelper.cs b/HyperVExtension/src/HyperVExtension/Helpers/PsObjectHelper.cs index 3c88fd8388..e3a1713603 100644 --- a/HyperVExtension/src/HyperVExtension/Helpers/PsObjectHelper.cs +++ b/HyperVExtension/src/HyperVExtension/Helpers/PsObjectHelper.cs @@ -71,7 +71,7 @@ public PsObjectHelper(in PSObject pSObject) catch (Exception ex) { var log = Log.ForContext("SourceContext", nameof(PsObjectHelper)); - log.Error($"Failed to get property value with name {propertyName} from object with type {type}.", ex); + log.Error(ex, $"Failed to get property value with name {propertyName} from object with type {type}."); } return default(T); diff --git a/HyperVExtension/src/HyperVExtension/Helpers/Resources.cs b/HyperVExtension/src/HyperVExtension/Helpers/Resources.cs index 570a98ecff..eeb03be0e0 100644 --- a/HyperVExtension/src/HyperVExtension/Helpers/Resources.cs +++ b/HyperVExtension/src/HyperVExtension/Helpers/Resources.cs @@ -23,7 +23,7 @@ public static string GetResource(string identifier, ILogger? log = null) } catch (Exception ex) { - log?.Error($"Failed loading resource: {identifier}", ex); + log?.Error(ex, $"Failed loading resource: {identifier}"); // If we fail, load the original identifier so it is obvious which resource is missing. return identifier; diff --git a/HyperVExtension/src/HyperVExtension/HyperVExtension.cs b/HyperVExtension/src/HyperVExtension/HyperVExtension.cs index 3bc6f6a097..e71c411242 100644 --- a/HyperVExtension/src/HyperVExtension/HyperVExtension.cs +++ b/HyperVExtension/src/HyperVExtension/HyperVExtension.cs @@ -57,7 +57,7 @@ public HyperVExtension(IHost host) } catch (Exception ex) { - log.Error($"Failed to get provider for provider type {providerType}", ex); + log.Error(ex, $"Failed to get provider for provider type {providerType}"); } return provider; diff --git a/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs b/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs index eccfa1458d..0e190d4bca 100644 --- a/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs +++ b/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs @@ -192,7 +192,7 @@ private ComputeSystemOperationResult Start(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Start), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Start)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } } @@ -253,7 +253,7 @@ public IAsyncOperation TerminateAsync(string optio catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Terminate), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Terminate)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -278,7 +278,7 @@ public IAsyncOperation DeleteAsync(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Delete), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Delete)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -309,7 +309,7 @@ public IAsyncOperation SaveAsync(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Save), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Save)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -340,7 +340,7 @@ public IAsyncOperation PauseAsync(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Pause), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Pause)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -371,7 +371,7 @@ public IAsyncOperation ResumeAsync(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Resume), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Resume)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -393,7 +393,7 @@ public IAsyncOperation CreateSnapshotAsync(string } catch (Exception ex) { - _log.Error(OperationErrorString(ComputeSystemOperations.CreateSnapshot), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.CreateSnapshot)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -416,7 +416,7 @@ public IAsyncOperation RevertSnapshotAsync(string } catch (Exception ex) { - _log.Error(OperationErrorString(ComputeSystemOperations.RevertSnapshot), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.RevertSnapshot)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -439,7 +439,7 @@ public IAsyncOperation DeleteSnapshotAsync(string } catch (Exception ex) { - _log.Error(OperationErrorString(ComputeSystemOperations.DeleteSnapshot), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.DeleteSnapshot)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -457,7 +457,7 @@ public IAsyncOperation ConnectAsync(string options } catch (Exception ex) { - _log.Error($"Failed to launch vmconnect on {DateTime.Now}: VM details: {this}", ex); + _log.Error(ex, $"Failed to launch vmconnect on {DateTime.Now}: VM details: {this}"); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -487,7 +487,7 @@ public IAsyncOperation RestartAsync(string options catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Restart), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Restart)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -536,7 +536,7 @@ public IAsyncOperation> GetComputeSystemPrope } catch (Exception ex) { - _log.Error($"Failed to GetComputeSystemPropertiesAsync on {DateTime.Now}: VM details: {this}", ex); + _log.Error(ex, $"Failed to GetComputeSystemPropertiesAsync on {DateTime.Now}: VM details: {this}"); return new List(); } }).AsAsyncOperation(); @@ -708,7 +708,7 @@ public SDK.ApplyConfigurationResult ApplyConfiguration(ApplyConfigurationOperati } catch (Exception ex) { - _log.Error($"Failed to apply configuration on {DateTime.Now}: VM details: {this}", ex); + _log.Error(ex, $"Failed to apply configuration on {DateTime.Now}: VM details: {this}"); return operation.CompleteOperation(new HostGuestCommunication.ApplyConfigurationResult(ex.HResult, ex.Message)); } } @@ -721,7 +721,7 @@ public SDK.ApplyConfigurationResult ApplyConfiguration(ApplyConfigurationOperati } catch (Exception ex) { - _log.Error($"Failed to apply configuration on {DateTime.Now}: VM details: {this}", ex); + _log.Error(ex, $"Failed to apply configuration on {DateTime.Now}: VM details: {this}"); return new ApplyConfigurationOperation(this, ex); } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs index 6e3202ff51..8bf3fcb3bc 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs @@ -139,7 +139,7 @@ public void Report(IOperationReport value) } catch (Exception ex) { - _log.Error("Operation to create compute system failed", ex); + _log.Error(ex, "Operation to create compute system failed"); ComputeSystemResult = new CreateComputeSystemResult(ex, ex.Message, ex.Message); } @@ -161,7 +161,7 @@ private void UpdateProgress(IOperationReport report, string localizedKey, string } catch (Exception ex) { - _log.Error("Failed to update progress", ex); + _log.Error(ex, "Failed to update progress"); } } @@ -173,7 +173,7 @@ private void UpdateProgress(string localizedString, uint percentage = 0u) } catch (Exception ex) { - _log.Error("Failed to update progress", ex); + _log.Error(ex, "Failed to update progress"); } } @@ -221,7 +221,7 @@ private async Task DeleteFileIfExists(StorageFile file) } catch (Exception ex) { - _log.Error($"Failed to delete file {file.Path}", ex); + _log.Error(ex, $"Failed to delete file {file.Path}"); } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VmCredentialAdaptiveCardSession.cs b/HyperVExtension/src/HyperVExtension/Models/VmCredentialAdaptiveCardSession.cs index 009dc78c95..b7be3a7ef1 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VmCredentialAdaptiveCardSession.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VmCredentialAdaptiveCardSession.cs @@ -163,7 +163,7 @@ public IAsyncOperation OnAction(string action, string i } catch (Exception ex) { - _log.Error($"Exception in OnAction: {ex}"); + _log.Error(ex, $"Exception in OnAction: {ex}"); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, "Something went wrong", ex.Message); } diff --git a/HyperVExtension/src/HyperVExtension/Models/WaitForLoginAdaptiveCardSession.cs b/HyperVExtension/src/HyperVExtension/Models/WaitForLoginAdaptiveCardSession.cs index 677aa14d11..c5baabb0f5 100644 --- a/HyperVExtension/src/HyperVExtension/Models/WaitForLoginAdaptiveCardSession.cs +++ b/HyperVExtension/src/HyperVExtension/Models/WaitForLoginAdaptiveCardSession.cs @@ -144,7 +144,7 @@ public IAsyncOperation OnAction(string action, string i } catch (Exception ex) { - _log.Error($"Exception in OnAction: {ex}"); + _log.Error(ex, $"Exception in OnAction: {ex}"); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, "Something went wrong", ex.Message); } diff --git a/HyperVExtension/src/HyperVExtension/Providers/HyperVProvider.cs b/HyperVExtension/src/HyperVExtension/Providers/HyperVProvider.cs index b9d5b778e1..dd00cd5376 100644 --- a/HyperVExtension/src/HyperVExtension/Providers/HyperVProvider.cs +++ b/HyperVExtension/src/HyperVExtension/Providers/HyperVProvider.cs @@ -75,7 +75,7 @@ public IAsyncOperation GetComputeSystemsAsync(IDeveloperId } catch (Exception ex) { - _log.Error($"Failed to retrieved all virtual machines on: {DateTime.Now}", ex); + _log.Error(ex, $"Failed to retrieved all virtual machines on: {DateTime.Now}"); return new ComputeSystemsResult(ex, OperationErrorString, ex.Message); } }).AsAsyncOperation(); @@ -105,7 +105,7 @@ public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForComputeSystem } catch (Exception ex) { - _log.Error($"Failed to create a new virtual machine on: {DateTime.Now}", ex); + _log.Error(ex, $"Failed to create a new virtual machine on: {DateTime.Now}"); // Dev Home will handle null values as failed operations. We can't throw because this is an out of proc // COM call, so we'll lose the error information. We'll log the error and return null. diff --git a/HyperVExtension/src/HyperVExtension/Services/PowerShellService.cs b/HyperVExtension/src/HyperVExtension/Services/PowerShellService.cs index dbfb896090..b665df70a7 100644 --- a/HyperVExtension/src/HyperVExtension/Services/PowerShellService.cs +++ b/HyperVExtension/src/HyperVExtension/Services/PowerShellService.cs @@ -60,7 +60,7 @@ public PowerShellResult Execute(IEnumerable comm catch (Exception ex) { var commandStrings = string.Join(Environment.NewLine, commandLineStatements.Select(cmd => cmd.ToString())); - _log.Error($"Error running PowerShell commands: {commandStrings}", ex); + _log.Error(ex, $"Error running PowerShell commands: {commandStrings}"); throw; } } diff --git a/HyperVExtension/src/HyperVExtension/Services/VMGalleryService.cs b/HyperVExtension/src/HyperVExtension/Services/VMGalleryService.cs index 6f1ba9c3f1..e09837ce43 100644 --- a/HyperVExtension/src/HyperVExtension/Services/VMGalleryService.cs +++ b/HyperVExtension/src/HyperVExtension/Services/VMGalleryService.cs @@ -87,7 +87,7 @@ public async Task GetGalleryImagesAsync() } catch (Exception ex) { - _log.Error($"Unable to retrieve VM gallery images", ex); + _log.Error(ex, $"Unable to retrieve VM gallery images"); } return _imageList; diff --git a/common/Environments/Helpers/ComputeSystemHelpers.cs b/common/Environments/Helpers/ComputeSystemHelpers.cs index f97ff096b7..779519de52 100644 --- a/common/Environments/Helpers/ComputeSystemHelpers.cs +++ b/common/Environments/Helpers/ComputeSystemHelpers.cs @@ -35,7 +35,7 @@ public static class ComputeSystemHelpers } catch (Exception ex) { - Log.Error($"Failed to get thumbnail for compute system {computeSystemWrapper}.", ex); + Log.Error(ex, $"Failed to get thumbnail for compute system {computeSystemWrapper}."); return null; } } @@ -56,7 +56,7 @@ public static async Task> GetComputeSystemPropertiesAsync(Com } catch (Exception ex) { - Log.Error($"Failed to get all properties for compute system {computeSystemWrapper}.", ex); + Log.Error(ex, $"Failed to get all properties for compute system {computeSystemWrapper}."); return propertyList; } } diff --git a/common/Environments/Helpers/StringResourceHelper.cs b/common/Environments/Helpers/StringResourceHelper.cs index 4281531028..3e1982327c 100644 --- a/common/Environments/Helpers/StringResourceHelper.cs +++ b/common/Environments/Helpers/StringResourceHelper.cs @@ -26,7 +26,7 @@ public static string GetResource(string key, params object[] args) } catch (Exception ex) { - Log.Error($"Failed to get resource for key {key}.", ex); + Log.Error(ex, $"Failed to get resource for key {key}."); return key; } } diff --git a/common/Environments/Models/CardProperty.cs b/common/Environments/Models/CardProperty.cs index 5b15d56153..2bf9f0c89b 100644 --- a/common/Environments/Models/CardProperty.cs +++ b/common/Environments/Models/CardProperty.cs @@ -142,7 +142,7 @@ public static unsafe BitmapImage ConvertMsResourceToIcon(Uri iconPathUri, string } catch (Exception ex) { - Log.Error($"Failed to load icon from ms-resource: {iconPathUri} for package: {packageFullName} due to error:", ex); + Log.Error(ex, $"Failed to load icon from ms-resource: {iconPathUri} for package: {packageFullName} due to error:"); } return new BitmapImage(); @@ -200,7 +200,7 @@ public string ConvertBytesToString(object? size) } catch (Exception ex) { - Log.Error($"Failed to convert size in bytes to ulong. Error: {ex}"); + Log.Error(ex, $"Failed to convert size in bytes to ulong. Error: {ex}"); return string.Empty; } } diff --git a/common/Environments/Models/ComputeSystem.cs b/common/Environments/Models/ComputeSystem.cs index 4cdf9d64e0..ae21a80073 100644 --- a/common/Environments/Models/ComputeSystem.cs +++ b/common/Environments/Models/ComputeSystem.cs @@ -63,7 +63,7 @@ public void OnComputeSystemStateChanged(object? sender, ComputeSystemState state } catch (Exception ex) { - _log.Error($"OnComputeSystemStateChanged for: {this} failed due to exception", ex); + _log.Error(ex, $"OnComputeSystemStateChanged for: {this} failed due to exception"); } } @@ -75,7 +75,7 @@ public async Task GetStateAsync() } catch (Exception ex) { - _log.Error($"GetStateAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"GetStateAsync for: {this} failed due to exception"); return new ComputeSystemStateResult(ex, errorString, ex.Message); } } @@ -88,7 +88,7 @@ public async Task StartAsync(string options) } catch (Exception ex) { - _log.Error($"StartAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"StartAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -101,7 +101,7 @@ public async Task ShutDownAsync(string options) } catch (Exception ex) { - _log.Error($"ShutDownAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"ShutDownAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -114,7 +114,7 @@ public async Task RestartAsync(string options) } catch (Exception ex) { - _log.Error($"RestartAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"RestartAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -127,7 +127,7 @@ public async Task TerminateAsync(string options) } catch (Exception ex) { - _log.Error($"TerminateAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"TerminateAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -140,7 +140,7 @@ public async Task DeleteAsync(string options) } catch (Exception ex) { - _log.Error($"DeleteAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"DeleteAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -153,7 +153,7 @@ public async Task SaveAsync(string options) } catch (Exception ex) { - _log.Error($"SaveAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"SaveAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -166,7 +166,7 @@ public async Task PauseAsync(string options) } catch (Exception ex) { - _log.Error($"PauseAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"PauseAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -179,7 +179,7 @@ public async Task ResumeAsync(string options) } catch (Exception ex) { - _log.Error($"ResumeAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"ResumeAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -192,7 +192,7 @@ public async Task CreateSnapshotAsync(string optio } catch (Exception ex) { - _log.Error($"CreateSnapshotAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"CreateSnapshotAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -205,7 +205,7 @@ public async Task RevertSnapshotAsync(string optio } catch (Exception ex) { - _log.Error($"RevertSnapshotAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"RevertSnapshotAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -218,7 +218,7 @@ public async Task DeleteSnapshotAsync(string optio } catch (Exception ex) { - _log.Error($"DeleteSnapshotAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"DeleteSnapshotAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -231,7 +231,7 @@ public async Task ModifyPropertiesAsync(string opt } catch (Exception ex) { - _log.Error($"ModifyPropertiesAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"ModifyPropertiesAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -244,7 +244,7 @@ public async Task GetComputeSystemThumbnailAsync(s } catch (Exception ex) { - _log.Error($"GetComputeSystemThumbnailAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"GetComputeSystemThumbnailAsync for: {this} failed due to exception"); return new ComputeSystemThumbnailResult(ex, errorString, ex.Message); } } @@ -257,7 +257,7 @@ public async Task> GetComputeSystemProperties } catch (Exception ex) { - _log.Error($"GetComputeSystemPropertiesAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"GetComputeSystemPropertiesAsync for: {this} failed due to exception"); return new List(); } } @@ -270,7 +270,7 @@ public async Task ConnectAsync(string options) } catch (Exception ex) { - _log.Error($"ConnectAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"ConnectAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -283,7 +283,7 @@ public IApplyConfigurationOperation ApplyConfiguration(string configuration) } catch (Exception ex) { - _log.Error($"ApplyConfiguration for: {this} failed due to exception", ex); + _log.Error(ex, $"ApplyConfiguration for: {this} failed due to exception"); throw; } } diff --git a/common/Environments/Models/ComputeSystemProvider.cs b/common/Environments/Models/ComputeSystemProvider.cs index 6d239be5d8..ad64203245 100644 --- a/common/Environments/Models/ComputeSystemProvider.cs +++ b/common/Environments/Models/ComputeSystemProvider.cs @@ -52,7 +52,7 @@ public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForDeveloperId(I } catch (Exception ex) { - _log.Error($"CreateAdaptiveCardSessionForDeveloperId for: {this} failed due to exception", ex); + _log.Error(ex, $"CreateAdaptiveCardSessionForDeveloperId for: {this} failed due to exception"); return new ComputeSystemAdaptiveCardResult(ex, errorString, ex.Message); } } @@ -65,7 +65,7 @@ public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForComputeSystem } catch (Exception ex) { - _log.Error($"CreateAdaptiveCardSessionForComputeSystem for: {this} failed due to exception", ex); + _log.Error(ex, $"CreateAdaptiveCardSessionForComputeSystem for: {this} failed due to exception"); return new ComputeSystemAdaptiveCardResult(ex, errorString, ex.Message); } } @@ -78,7 +78,7 @@ public async Task GetComputeSystemsAsync(IDeveloperId deve } catch (Exception ex) { - _log.Error($"GetComputeSystemsAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"GetComputeSystemsAsync for: {this} failed due to exception"); return new ComputeSystemsResult(ex, errorString, ex.Message); } } @@ -92,7 +92,7 @@ public async Task GetComputeSystemsAsync(IDeveloperId deve } catch (Exception ex) { - _log.Error($"GetComputeSystemsAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"GetComputeSystemsAsync for: {this} failed due to exception"); return new FailedCreateComputeSystemOperation(ex, StringResourceHelper.GetResource("CreationOperationStoppedUnexpectedly")); } } diff --git a/common/Environments/Models/CreateComputeSystemOperation.cs b/common/Environments/Models/CreateComputeSystemOperation.cs index 3f93b4a72f..ecf69adc04 100644 --- a/common/Environments/Models/CreateComputeSystemOperation.cs +++ b/common/Environments/Models/CreateComputeSystemOperation.cs @@ -112,7 +112,7 @@ public void StartOperation() } catch (Exception ex) { - _log.Error($"StartOperation failed for provider {ProviderDetails.ComputeSystemProvider}", ex); + _log.Error(ex, $"StartOperation failed for provider {ProviderDetails.ComputeSystemProvider}"); CreateComputeSystemResult = new CreateComputeSystemResult(ex, StringResourceHelper.GetResource("CreationOperationStoppedUnexpectedly"), ex.Message); Completed?.Invoke(this, CreateComputeSystemResult); } @@ -145,7 +145,7 @@ public void RemoveEventHandlers() } catch (Exception ex) { - _log.Error($"Failed to remove event handlers for {this}", ex); + _log.Error(ex, $"Failed to remove event handlers for {this}"); } } @@ -157,7 +157,7 @@ public void CancelOperation() } catch (Exception ex) { - _log.Error($"Failed to cancel operation for {this}", ex); + _log.Error(ex, $"Failed to cancel operation for {this}"); } } diff --git a/common/Environments/Services/ComputeSystemManager.cs b/common/Environments/Services/ComputeSystemManager.cs index 45861aad6a..616566deae 100644 --- a/common/Environments/Services/ComputeSystemManager.cs +++ b/common/Environments/Services/ComputeSystemManager.cs @@ -77,17 +77,17 @@ await Parallel.ForEachAsync(computeSystemsProviderDetails, async (providerDetail { if (innerEx is TaskCanceledException) { - _log.Error($"Failed to get retrieve all compute systems from all compute system providers due to cancellation", innerEx); + _log.Error(innerEx, $"Failed to get retrieve all compute systems from all compute system providers due to cancellation"); } else { - _log.Error($"Failed to get retrieve all compute systems from all compute system providers ", innerEx); + _log.Error(innerEx, $"Failed to get retrieve all compute systems from all compute system providers "); } } } catch (Exception ex) { - _log.Error($"Failed to get retrieve all compute systems from all compute system providers ", ex); + _log.Error(ex, $"Failed to get retrieve all compute systems from all compute system providers "); } } diff --git a/common/Helpers/AdaptiveCardHelpers.cs b/common/Helpers/AdaptiveCardHelpers.cs index a662b5e7f9..2332714e8d 100644 --- a/common/Helpers/AdaptiveCardHelpers.cs +++ b/common/Helpers/AdaptiveCardHelpers.cs @@ -32,7 +32,7 @@ public static ImageIcon ConvertBase64StringToImageIcon(string base64String) } catch (Exception ex) { - _log.Error($"Failed to load image icon", ex); + _log.Error(ex, $"Failed to load image icon"); return new ImageIcon(); } } diff --git a/common/Helpers/Deployment.cs b/common/Helpers/Deployment.cs index c62eef22f7..c25cb02e3f 100644 --- a/common/Helpers/Deployment.cs +++ b/common/Helpers/Deployment.cs @@ -37,7 +37,7 @@ public static Guid Identifier // We do not want this identifer's access to ever create a problem in the // application, so if we can't get it, return empty guid. An empty guid is also a // signal that the data is unknown for filtering purposes. - Log.Error($"Failed getting Deployment Identifier", ex); + Log.Error(ex, $"Failed getting Deployment Identifier"); return Guid.Empty; } } diff --git a/common/Models/ExtensionAdaptiveCardSession.cs b/common/Models/ExtensionAdaptiveCardSession.cs index 9494a1c99c..089308e6fb 100644 --- a/common/Models/ExtensionAdaptiveCardSession.cs +++ b/common/Models/ExtensionAdaptiveCardSession.cs @@ -21,8 +21,6 @@ public class ExtensionAdaptiveCardSession { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ExtensionAdaptiveCardSession)); - private readonly string _componentName = "ExtensionAdaptiveCardSession"; - public IExtensionAdaptiveCardSession Session { get; private set; } public event TypedEventHandler? Stopped; @@ -45,7 +43,7 @@ public ProviderOperationResult Initialize(IExtensionAdaptiveCard extensionUI) } catch (Exception ex) { - _log.Error(_componentName, $"Initialize failed due to exception", ex); + _log.Error(ex, $"Initialize failed due to exception"); return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, ex.Message, ex.Message); } } @@ -63,7 +61,7 @@ public void Dispose() } catch (Exception ex) { - _log.Error(_componentName, $"Dispose failed due to exception", ex); + _log.Error(ex, $"Dispose failed due to exception"); } } @@ -75,7 +73,7 @@ public async Task OnAction(string action, string inputs } catch (Exception ex) { - _log.Error(_componentName, $"OnAction failed due to exception", ex); + _log.Error(ex, $"OnAction failed due to exception"); return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, ex.Message, ex.Message); } } diff --git a/common/Services/AdaptiveCardRenderingService.cs b/common/Services/AdaptiveCardRenderingService.cs index 19923e01bd..3e712f476d 100644 --- a/common/Services/AdaptiveCardRenderingService.cs +++ b/common/Services/AdaptiveCardRenderingService.cs @@ -119,7 +119,7 @@ private async Task UpdateHostConfig() } catch (Exception ex) { - _log.Error("Error retrieving HostConfig", ex); + _log.Error(ex, "Error retrieving HostConfig"); } _windowEx.DispatcherQueue.TryEnqueue(() => diff --git a/common/Services/AppInstallManagerService.cs b/common/Services/AppInstallManagerService.cs index a16f63746b..4e8e017efa 100644 --- a/common/Services/AppInstallManagerService.cs +++ b/common/Services/AppInstallManagerService.cs @@ -97,7 +97,7 @@ public async Task TryInstallPackageAsync(string packageId) } catch (Exception ex) { - _log.Error("Package installation Failed", ex); + _log.Error(ex, "Package installation Failed"); } return false; diff --git a/common/Services/ComputeSystemService.cs b/common/Services/ComputeSystemService.cs index 42be48d4b5..395b68f1fa 100644 --- a/common/Services/ComputeSystemService.cs +++ b/common/Services/ComputeSystemService.cs @@ -83,7 +83,7 @@ public async Task> GetComputeSystemProvidersA } catch (Exception ex) { - Log.Error($"Failed to get {nameof(IComputeSystemProvider)} provider from '{extension.PackageFamilyName}/{extension.ExtensionDisplayName}'", ex); + Log.Error(ex, $"Failed to get {nameof(IComputeSystemProvider)} provider from '{extension.PackageFamilyName}/{extension.ExtensionDisplayName}'"); } } diff --git a/common/Services/NotificationService.cs b/common/Services/NotificationService.cs index 2da857e013..72ff9e577b 100644 --- a/common/Services/NotificationService.cs +++ b/common/Services/NotificationService.cs @@ -25,8 +25,6 @@ public class NotificationService private readonly IWindowsIdentityService _windowsIdentityService; - private readonly string _componentName = "NotificationService"; - private readonly string _hyperVText = "Hyper-V"; private readonly string _microsoftText = "Microsoft"; @@ -81,7 +79,7 @@ public void HandlerNotificationActions(AppActivationArguments args) } catch (Exception ex) { - _log.Error(_componentName, $"Unable to launch computer management due to exception", ex); + _log.Error(ex, $"Unable to launch computer management due to exception"); } } } @@ -124,7 +122,7 @@ public void ShowRestartNotification() } else { - _log.Error(_componentName, "Notification queue is not initialized"); + _log.Error("Notification queue is not initialized"); } } @@ -149,7 +147,7 @@ public void CheckIfUserIsAHyperVAdminAndShowNotification() var user = _windowsIdentityService.GetCurrentUserName(); if (user == null) { - _log.Error(_componentName, "Unable to get the current user name"); + _log.Error("Unable to get the current user name"); return; } @@ -185,7 +183,7 @@ public void CheckIfUserIsAHyperVAdminAndShowNotification() } catch (Exception ex) { - _log.Error(_componentName, "Unable to add the user to the Hyper-V Administrators group", ex); + _log.Error(ex, "Unable to add the user to the Hyper-V Administrators group"); ShowUnableToAddToHyperVAdminGroupNotification(); } }); @@ -209,7 +207,7 @@ public void CheckIfUserIsAHyperVAdminAndShowNotification() } else { - _log.Error(_componentName, "Notification queue is not initialized"); + _log.Error("Notification queue is not initialized"); } } } diff --git a/extensions/CoreWidgetProvider/Helpers/GPUStats.cs b/extensions/CoreWidgetProvider/Helpers/GPUStats.cs index 972c7320a7..6cfaf019e7 100644 --- a/extensions/CoreWidgetProvider/Helpers/GPUStats.cs +++ b/extensions/CoreWidgetProvider/Helpers/GPUStats.cs @@ -113,7 +113,7 @@ public void GetData() } catch (InvalidOperationException ex) { - Log.Warning("GPUStats", "Failed to get next value", ex); + Log.Warning(ex, "GPUStats", "Failed to get next value"); Log.Information("GPUStats", "Calling GetGPUPerfCounters again"); GetGPUPerfCounters(); } diff --git a/extensions/CoreWidgetProvider/Helpers/NetworkStats.cs b/extensions/CoreWidgetProvider/Helpers/NetworkStats.cs index 2b0f5b2795..1b56389faa 100644 --- a/extensions/CoreWidgetProvider/Helpers/NetworkStats.cs +++ b/extensions/CoreWidgetProvider/Helpers/NetworkStats.cs @@ -87,7 +87,7 @@ public void GetData() } catch (Exception ex) { - Log.Error("Error getting network data.", ex); + Log.Error(ex, "Error getting network data."); } } } diff --git a/extensions/CoreWidgetProvider/Helpers/Resources.cs b/extensions/CoreWidgetProvider/Helpers/Resources.cs index 90e2144d1c..85e5659de3 100644 --- a/extensions/CoreWidgetProvider/Helpers/Resources.cs +++ b/extensions/CoreWidgetProvider/Helpers/Resources.cs @@ -23,7 +23,7 @@ public static string GetResource(string identifier, ILogger? log = null) } catch (Exception ex) { - log?.Error($"Failed loading resource: {identifier}", ex); + log?.Error(ex, $"Failed loading resource: {identifier}"); // If we fail, load the original identifier so it is obvious which resource is missing. return identifier; diff --git a/extensions/CoreWidgetProvider/Widgets/CoreWidget.cs b/extensions/CoreWidgetProvider/Widgets/CoreWidget.cs index c223040362..9700800557 100644 --- a/extensions/CoreWidgetProvider/Widgets/CoreWidget.cs +++ b/extensions/CoreWidgetProvider/Widgets/CoreWidget.cs @@ -140,7 +140,7 @@ protected string GetTemplateForPage(WidgetPageState page) } catch (Exception e) { - Log.Error("Error getting template.", e); + Log.Error(e, "Error getting template."); return string.Empty; } } diff --git a/extensions/CoreWidgetProvider/Widgets/SSHWalletWidget.cs b/extensions/CoreWidgetProvider/Widgets/SSHWalletWidget.cs index f8623fd426..09818b1e0c 100644 --- a/extensions/CoreWidgetProvider/Widgets/SSHWalletWidget.cs +++ b/extensions/CoreWidgetProvider/Widgets/SSHWalletWidget.cs @@ -83,7 +83,7 @@ public override void LoadContentData() } catch (Exception e) { - Log.Error("Error retrieving data.", e); + Log.Error(e, "Error retrieving data."); var content = new JsonObject { { "errorMessage", e.Message }, @@ -309,7 +309,7 @@ public override string GetConfiguration(string data) } catch (Exception ex) { - Log.Error($"Failed getting configuration information for input config file path: {data}", ex); + Log.Error(ex, $"Failed getting configuration information for input config file path: {data}"); configurationData = FillConfigurationData(false, data, 0, Resources.GetResource(@"SSH_Widget_Template/ErrorProcessingConfigFile", Log)); diff --git a/extensions/CoreWidgetProvider/Widgets/SystemCPUUsageWidget.cs b/extensions/CoreWidgetProvider/Widgets/SystemCPUUsageWidget.cs index bc6f137a21..6f5052fea8 100644 --- a/extensions/CoreWidgetProvider/Widgets/SystemCPUUsageWidget.cs +++ b/extensions/CoreWidgetProvider/Widgets/SystemCPUUsageWidget.cs @@ -56,7 +56,7 @@ public override void LoadContentData() } catch (Exception e) { - Log.Error("Error retrieving stats.", e); + Log.Error(e, "Error retrieving stats."); var content = new JsonObject { { "errorMessage", e.Message }, diff --git a/extensions/CoreWidgetProvider/Widgets/SystemGPUUsageWidget.cs b/extensions/CoreWidgetProvider/Widgets/SystemGPUUsageWidget.cs index c3f24fddff..2bd44d80b5 100644 --- a/extensions/CoreWidgetProvider/Widgets/SystemGPUUsageWidget.cs +++ b/extensions/CoreWidgetProvider/Widgets/SystemGPUUsageWidget.cs @@ -58,7 +58,7 @@ public override void LoadContentData() } catch (Exception e) { - Log.Error("Error retrieving data.", e); + Log.Error(e, "Error retrieving data."); var content = new JsonObject { { "errorMessage", e.Message }, diff --git a/extensions/CoreWidgetProvider/Widgets/SystemMemoryWidget.cs b/extensions/CoreWidgetProvider/Widgets/SystemMemoryWidget.cs index a24210b9e8..88112f6ce9 100644 --- a/extensions/CoreWidgetProvider/Widgets/SystemMemoryWidget.cs +++ b/extensions/CoreWidgetProvider/Widgets/SystemMemoryWidget.cs @@ -75,7 +75,7 @@ public override void LoadContentData() } catch (Exception e) { - Log.Error("Error retrieving data.", e); + Log.Error(e, "Error retrieving data."); var content = new JsonObject { { "errorMessage", e.Message }, diff --git a/extensions/CoreWidgetProvider/Widgets/SystemNetworkUsageWidget.cs b/extensions/CoreWidgetProvider/Widgets/SystemNetworkUsageWidget.cs index 2e56c47444..1161487f48 100644 --- a/extensions/CoreWidgetProvider/Widgets/SystemNetworkUsageWidget.cs +++ b/extensions/CoreWidgetProvider/Widgets/SystemNetworkUsageWidget.cs @@ -76,7 +76,7 @@ public override void LoadContentData() } catch (Exception e) { - Log.Error("Error retrieving data.", e); + Log.Error(e, "Error retrieving data."); var content = new JsonObject { { "errorMessage", e.Message }, diff --git a/extensions/CoreWidgetProvider/Widgets/WidgetProvider.cs b/extensions/CoreWidgetProvider/Widgets/WidgetProvider.cs index 623bbdd3fd..7f55948f34 100644 --- a/extensions/CoreWidgetProvider/Widgets/WidgetProvider.cs +++ b/extensions/CoreWidgetProvider/Widgets/WidgetProvider.cs @@ -64,7 +64,7 @@ private void RecoverRunningWidgets() } catch (Exception e) { - Log.Error("Failed retrieving list of running widgets.", e); + Log.Error(e, "Failed retrieving list of running widgets."); return; } diff --git a/settings/DevHome.Settings/Views/AboutPage.xaml.cs b/settings/DevHome.Settings/Views/AboutPage.xaml.cs index ec2046d7a3..743dc23bb1 100644 --- a/settings/DevHome.Settings/Views/AboutPage.xaml.cs +++ b/settings/DevHome.Settings/Views/AboutPage.xaml.cs @@ -44,7 +44,7 @@ private void OpenLogsLocation() catch (Exception e) { var log = Log.ForContext("SourceContext", "AboutPage"); - log.Error($"Error opening log location", e); + log.Error(e, $"Error opening log location"); } } #endif diff --git a/settings/DevHome.Settings/Views/AccountsPage.xaml.cs b/settings/DevHome.Settings/Views/AccountsPage.xaml.cs index f37a358c69..427aa761bb 100644 --- a/settings/DevHome.Settings/Views/AccountsPage.xaml.cs +++ b/settings/DevHome.Settings/Views/AccountsPage.xaml.cs @@ -114,7 +114,7 @@ public async Task ShowLoginUIAsync(string loginEntryPoint, Page parentPage, Acco } catch (Exception ex) { - _log.Error($"ShowLoginUIAsync(): loginUIContentDialog failed.", ex); + _log.Error(ex, $"ShowLoginUIAsync(): loginUIContentDialog failed."); } accountProvider.RefreshLoggedInAccounts(); @@ -186,7 +186,7 @@ private async Task InitiateAddAccountUserExperienceAsync(Page parentPage, Accoun } catch (Exception ex) { - _log.Error($"Exception thrown while calling {nameof(accountProvider.DeveloperIdProvider)}.{nameof(accountProvider.DeveloperIdProvider.ShowLogonSession)}: ", ex); + _log.Error(ex, $"Exception thrown while calling {nameof(accountProvider.DeveloperIdProvider)}.{nameof(accountProvider.DeveloperIdProvider.ShowLogonSession)}: "); } accountProvider.RefreshLoggedInAccounts(); diff --git a/src/Services/AccountsService.cs b/src/Services/AccountsService.cs index 0c628f7a5e..9574232fbb 100644 --- a/src/Services/AccountsService.cs +++ b/src/Services/AccountsService.cs @@ -59,7 +59,7 @@ public async Task> GetDevIdProviders() } catch (Exception ex) { - _log.Error($"Failed to get {nameof(IDeveloperIdProvider)} provider from '{extension.PackageFamilyName}/{extension.ExtensionDisplayName}'", ex); + _log.Error(ex, $"Failed to get {nameof(IDeveloperIdProvider)} provider from '{extension.PackageFamilyName}/{extension.ExtensionDisplayName}'"); } } diff --git a/src/Services/DSCFileActivationHandler.cs b/src/Services/DSCFileActivationHandler.cs index 2c366c7ce6..1731684323 100644 --- a/src/Services/DSCFileActivationHandler.cs +++ b/src/Services/DSCFileActivationHandler.cs @@ -98,7 +98,7 @@ await _mainWindow.ShowErrorMessageDialogAsync( } catch (Exception ex) { - _log.Error("Error executing the DSC activation flow", ex); + _log.Error(ex, "Error executing the DSC activation flow"); } } } diff --git a/src/ViewModels/InitializationViewModel.cs b/src/ViewModels/InitializationViewModel.cs index 025c6c616c..7c0b33a0cd 100644 --- a/src/ViewModels/InitializationViewModel.cs +++ b/src/ViewModels/InitializationViewModel.cs @@ -65,7 +65,7 @@ public async void OnPageLoaded() } catch (Exception ex) { - _log.Information("Installing WidgetService failed: ", ex); + _log.Information(ex, "Installing WidgetService failed: "); } // Install the DevHomeGitHubExtension, unless it's already installed or a dev build is running. @@ -82,7 +82,7 @@ public async void OnPageLoaded() } catch (Exception ex) { - _log.Information("Installing DevHomeGitHubExtension failed: ", ex); + _log.Information(ex, "Installing DevHomeGitHubExtension failed: "); } } diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs index 2d0cedc5c4..46c5c7c000 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs @@ -113,7 +113,7 @@ private void MoveDirectory(string sourceDirectory, string targetDirectory) } catch (Exception ex) { - Log.Error($"Error in MoveDirectory. Error: {ex}"); + Log.Error(ex, $"Error in MoveDirectory. Error: {ex}"); } } @@ -125,7 +125,7 @@ private void SetEnvironmentVariable(string variableName, string value) } catch (Exception ex) { - Log.Error($"Error in SetEnvironmentVariable. Error: {ex}"); + Log.Error(ex, $"Error in SetEnvironmentVariable. Error: {ex}"); } } diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs index 5e3fa54bd4..368eb7c1c1 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs @@ -175,7 +175,7 @@ public void LoadAllDevDrivesInTheUI() } catch (Exception ex) { - Log.Error($"Error loading Dev Drives data. Error: {ex}"); + Log.Error(ex, $"Error loading Dev Drives data. Error: {ex}"); } } @@ -195,7 +195,7 @@ public void LoadAllDevDriveOptimizersInTheUI() } catch (Exception ex) { - Log.Error($"Error loading Dev Drive Optimizers data. Error: {ex}"); + Log.Error(ex, $"Error loading Dev Drive Optimizers data. Error: {ex}"); } } @@ -215,7 +215,7 @@ public void LoadAllDevDriveOptimizedsInTheUI() } catch (Exception ex) { - Log.Error($"Error loading Dev Drive Optimized data. Error: {ex}"); + Log.Error(ex, $"Error loading Dev Drive Optimized data. Error: {ex}"); } } diff --git a/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs index 11a9ac4b8e..c7d360562a 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs @@ -110,7 +110,7 @@ public void QuietButtonClicked() catch (Exception ex) { SessionStateText = GetStatusString("SessionError"); - _log.Error("QuietBackgroundProcessesSession::Start failed", ex); + _log.Error(ex, "QuietBackgroundProcessesSession::Start failed"); } } else @@ -124,7 +124,7 @@ public void QuietButtonClicked() catch (Exception ex) { SessionStateText = GetStatusString("UnableToCancelSession"); - _log.Error("QuietBackgroundProcessesSession::Stop failed", ex); + _log.Error(ex, "QuietBackgroundProcessesSession::Stop failed"); } } } @@ -142,7 +142,7 @@ private bool GetIsActive() catch (Exception ex) { SessionStateText = GetStatusString("SessionError"); - _log.Error("QuietBackgroundProcessesSession::IsActive failed", ex); + _log.Error(ex, "QuietBackgroundProcessesSession::IsActive failed"); } return false; @@ -157,7 +157,7 @@ private int GetTimeRemaining() catch (Exception ex) { SessionStateText = GetStatusString("SessionError"); - _log.Error("QuietBackgroundProcessesSession::TimeLeftInSeconds failed", ex); + _log.Error(ex, "QuietBackgroundProcessesSession::TimeLeftInSeconds failed"); return 0; } } diff --git a/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs index d1e012ffa8..b4a2471d87 100644 --- a/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs @@ -115,7 +115,7 @@ private async void OnRemoveWidgetClick(object sender, RoutedEventArgs e) } catch (Exception ex) { - _log.Error($"Didn't delete Widget {widgetIdToDelete}", ex); + _log.Error(ex, $"Didn't delete Widget {widgetIdToDelete}"); } } } diff --git a/tools/Dashboard/DevHome.Dashboard/Services/WidgetAdaptiveCardRenderingService.cs b/tools/Dashboard/DevHome.Dashboard/Services/WidgetAdaptiveCardRenderingService.cs index 9295b65842..30d5e77417 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/WidgetAdaptiveCardRenderingService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/WidgetAdaptiveCardRenderingService.cs @@ -115,7 +115,7 @@ private async Task UpdateHostConfig() } catch (Exception ex) { - _log.Error("Error retrieving HostConfig", ex); + _log.Error(ex, "Error retrieving HostConfig"); } _windowEx.DispatcherQueue.TryEnqueue(() => diff --git a/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs b/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs index 175e47f5a7..2099ed7bef 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs @@ -119,7 +119,7 @@ public async Task GetWidgetHostAsync() } catch (Exception ex) { - _log.Error("Exception in WidgetHost.Register:", ex); + _log.Error(ex, "Exception in WidgetHost.Register:"); } } @@ -136,7 +136,7 @@ public async Task GetWidgetCatalogAsync() } catch (Exception ex) { - _log.Error("Exception in WidgetCatalog.GetDefault:", ex); + _log.Error(ex, "Exception in WidgetCatalog.GetDefault:"); } } diff --git a/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs b/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs index 5d48d0e1f9..a5197d6f9b 100644 --- a/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs +++ b/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs @@ -155,7 +155,7 @@ await Task.Run(async () => } catch (Exception ex) { - _log.Warning("There was an error expanding the Widget template with data: ", ex); + _log.Warning(ex, "There was an error expanding the Widget template with data: "); ShowErrorCard("WidgetErrorCardDisplayText"); return; } @@ -192,7 +192,7 @@ await Task.Run(async () => } catch (Exception ex) { - _log.Error("Error rendering widget card: ", ex); + _log.Error(ex, "Error rendering widget card: "); WidgetFrameworkElement = GetErrorCard("WidgetErrorCardDisplayText"); } }); diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs index 44c3bcafe0..b42a0effad 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs @@ -94,7 +94,7 @@ private async Task SubscribeToWidgetCatalogEventsAsync() } catch (Exception ex) { - _log.Error("Exception in SubscribeToWidgetCatalogEvents:", ex); + _log.Error(ex, "Exception in SubscribeToWidgetCatalogEvents:"); return false; } @@ -287,7 +287,7 @@ private async Task RestorePinnedWidgetsAsync(Widget[] hostWidgets) } catch (Exception ex) { - _log.Error($"RestorePinnedWidgets(): ", ex); + _log.Error(ex, $"RestorePinnedWidgets(): "); } } @@ -374,7 +374,7 @@ private async Task PinDefaultWidgetAsync(WidgetDefinition defaultWidgetDefinitio } catch (Exception ex) { - _log.Error($"PinDefaultWidget failed: ", ex); + _log.Error(ex, $"PinDefaultWidget failed: "); } } @@ -424,7 +424,7 @@ public async Task AddWidgetClickAsync() } catch (Exception ex) { - _log.Warning($"Creating widget failed: ", ex); + _log.Warning(ex, $"Creating widget failed: "); var mainWindow = Application.Current.GetService(); var stringResource = new StringResource("DevHome.Dashboard.pri", "DevHome.Dashboard/Resources"); await mainWindow.ShowErrorMessageDialogAsync( @@ -464,7 +464,7 @@ await Task.Run(async () => { // TODO Support concurrency in dashboard. Today concurrent async execution can cause insertion errors. // https://github.com/microsoft/devhome/issues/1215 - _log.Warning($"Couldn't insert pinned widget", ex); + _log.Warning(ex, $"Couldn't insert pinned widget"); } }); } @@ -479,7 +479,7 @@ await Task.Run(async () => } catch (Exception ex) { - _log.Information($"Error deleting widget", ex); + _log.Information(ex, $"Error deleting widget"); } } }); diff --git a/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs index 1dfbc4f9e7..3b0d268cfe 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs @@ -261,7 +261,7 @@ private async Task AddAllComputeSystemsFromAProvider(ComputeSystemsLoadedData da var result = mapping.Value.Result; await _notificationService.ShowNotificationAsync(provider.DisplayName, result.DisplayMessage, InfoBarSeverity.Error); - _log.Error($"Error occurred while adding Compute systems to environments page for provider: {provider.Id}", result.DiagnosticText, result.ExtendedError); + _log.Error($"Error occurred while adding Compute systems to environments page for provider: {provider.Id}. {result.DiagnosticText}, {result.ExtendedError}"); data.DevIdToComputeSystemMap.Remove(mapping.Key); } @@ -296,7 +296,7 @@ await _windowEx.DispatcherQueue.EnqueueAsync(async () => } catch (Exception ex) { - _log.Error($"Exception occurred while adding Compute systems to environments page for provider: {provider.Id}", ex); + _log.Error(ex, $"Exception occurred while adding Compute systems to environments page for provider: {provider.Id}"); } }); } diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs index 01f434838a..f9a50252c7 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs @@ -124,7 +124,7 @@ private async Task GetStoreData() } catch (Exception ex) { - _log.Error("Error retrieving packages", ex); + _log.Error(ex, "Error retrieving packages"); ShouldShowStoreError = true; } diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/DevDriveFormatter/DevDriveFormatter.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/DevDriveFormatter/DevDriveFormatter.cs index e8f8027d7e..f5d356cfb6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.Common/DevDriveFormatter/DevDriveFormatter.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/DevDriveFormatter/DevDriveFormatter.cs @@ -88,7 +88,7 @@ public int FormatPartitionAsDevDrive(char curDriveLetter, string driveLabel) } catch (CimException e) { - _log.Error($"A CimException occurred while formatting Dev Drive Error.", e); + _log.Error(e, $"A CimException occurred while formatting Dev Drive Error."); return e.HResult; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Elevation/IPCSetup.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/Elevation/IPCSetup.cs index 602d3f250b..d7acd95fd4 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.Common/Elevation/IPCSetup.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Elevation/IPCSetup.cs @@ -296,7 +296,7 @@ public static (RemoteObject, Process) CreateOutOfProcessObjectAndGetProcess( } catch (Exception e) { - Log.Error($"Error occurred during setup.", e); + Log.Error(e, $"Error occurred during setup."); mappedMemory.HResult = e.HResult; } diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs index ff4a8fcd59..9e848df2dd 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs @@ -43,7 +43,7 @@ public ElevatedComponentOperation(IList tasksArgumentList) } catch (Exception e) { - _log.Error($"Failed to parse tasks arguments", e); + _log.Error(e, $"Failed to parse tasks arguments"); throw; } } @@ -182,7 +182,7 @@ private async Task ValidateAndExecuteAsync( } catch (Exception e) { - _log.Error($"Failed to validate or execute operation", e); + _log.Error(e, $"Failed to validate or execute operation"); EndOperation(taskArguments, false); throw; } diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedConfigurationTask.cs b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedConfigurationTask.cs index 4238de1c5e..bad73c3373 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedConfigurationTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedConfigurationTask.cs @@ -56,7 +56,7 @@ public IAsyncOperation ApplyConfiguration(string fi } catch (Exception e) { - log.Error($"Failed to apply configuration.", e); + log.Error(e, $"Failed to apply configuration."); taskResult.TaskSucceeded = false; } diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs index 588cdab355..a90e148f7e 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs @@ -100,7 +100,7 @@ public IAsyncOperation InstallPackage(string packageI } catch (Exception e) { - _log.Error("Elevated app install failed.", e); + _log.Error(e, "Elevated app install failed."); result.TaskSucceeded = false; } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs index 0a9e2f0853..440dd217f7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs @@ -232,7 +232,7 @@ IAsyncOperation ISetupTask.Execute() if (result.Status == ProviderOperationStatus.Failure) { - _log.Error($"Could not clone {RepositoryToClone.DisplayName} because {result.DisplayMessage}", result.ExtendedError); + _log.Error(result.ExtendedError, $"Could not clone {RepositoryToClone.DisplayName} because {result.DisplayMessage}"); TelemetryFactory.Get().LogError("CloneTask_CouldNotClone_Event", LogLevel.Critical, new ExceptionEvent(result.ExtendedError.HResult, result.DisplayMessage)); _actionCenterErrorMessage.PrimaryMessage = _stringResource.GetLocalized(StringResourceKey.CloneRepoErrorForActionCenter, RepositoryToClone.DisplayName, result.DisplayMessage); @@ -242,7 +242,7 @@ IAsyncOperation ISetupTask.Execute() } catch (Exception e) { - _log.Error($"Could not clone {RepositoryToClone.DisplayName}", e); + _log.Error(e, $"Could not clone {RepositoryToClone.DisplayName}"); _actionCenterErrorMessage.PrimaryMessage = _stringResource.GetLocalized(StringResourceKey.CloneRepoErrorForActionCenter, RepositoryToClone.DisplayName, e.HResult.ToString("X", CultureInfo.CurrentCulture)); TelemetryFactory.Get().LogError("CloneTask_CouldNotClone_Event", LogLevel.Critical, new ExceptionEvent(e.HResult)); return TaskFinishedState.Failure; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CloningInformation.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CloningInformation.cs index eebbf5f94c..a88c3c2d62 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CloningInformation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CloningInformation.cs @@ -124,7 +124,7 @@ public void SetIcon(ElementTheme theme) } catch (Exception e) { - _log.Error(e.Message, e); + _log.Error(e, e.Message); RepositoryTypeIcon = GetGitIcon(theme); return; } @@ -259,7 +259,7 @@ public string RepositoryProviderDisplayName } catch (Exception e) { - _log.Error(e.Message, e); + _log.Error(e, e.Message); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs index a7fe05d628..c97ca72986 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs @@ -214,7 +214,7 @@ public void OnApplyConfigurationOperationChanged(object sender, SDK.Configuratio } catch (Exception ex) { - _log.Error($"Failed to process configuration progress data on target machine.'{ComputeSystemName}'", ex); + _log.Error(ex, $"Failed to process configuration progress data on target machine.'{ComputeSystemName}'"); } } @@ -254,7 +254,7 @@ public void HandleCompletedOperation(SDK.ApplyConfigurationResult applyConfigura if (resultStatus == ProviderOperationStatus.Failure) { - _log.Error($"Extension failed to configure config file with exception. Diagnostic text: {result.DiagnosticText}", result.ExtendedError); + _log.Error(result.ExtendedError, $"Extension failed to configure config file with exception. Diagnostic text: {result.DiagnosticText}"); throw new SDKApplyConfigurationSetResultException(applyConfigurationResult.Result.DiagnosticText); } @@ -288,7 +288,7 @@ public void HandleCompletedOperation(SDK.ApplyConfigurationResult applyConfigura } catch (Exception ex) { - _log.Error($"Failed to apply configuration on target machine. '{ComputeSystemName}'", ex); + _log.Error(ex, $"Failed to apply configuration on target machine. '{ComputeSystemName}'"); } var tempResultInfo = !string.IsNullOrEmpty(resultInformation) ? resultInformation : string.Empty; @@ -371,7 +371,7 @@ public IAsyncOperation Execute() } catch (Exception e) { - _log.Error($"Failed to apply configuration on target machine.", e); + _log.Error(e, $"Failed to apply configuration on target machine."); return TaskFinishedState.Failure; } }).AsAsyncOperation(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs index 2235ebeeac..353e74ba90 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs @@ -107,7 +107,7 @@ IAsyncOperation ISetupTask.Execute() } catch (Exception e) { - _log.Error($"Failed to apply configuration.", e); + _log.Error(e, $"Failed to apply configuration."); return TaskFinishedState.Failure; } }).AsAsyncOperation(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs index 19e6869af9..bd43532b02 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs @@ -129,7 +129,7 @@ IAsyncOperation ISetupTask.ExecuteAsAdmin(IElevatedComponentO catch (Exception ex) { result = ex.HResult; - _log.Error($"Failed to create Dev Drive.", ex); + _log.Error(ex, $"Failed to create Dev Drive."); _actionCenterMessages.PrimaryMessage = _stringResource.GetLocalized(StringResourceKey.DevDriveErrorWithReason, _stringResource.GetLocalizedErrorMsg(ex.HResult, Identity.Component.DevDrive)); TelemetryFactory.Get().LogException("CreatingDevDriveException", ex); return TaskFinishedState.Failure; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/GenericRepository.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/GenericRepository.cs index d37f37fe95..cfb9c611f6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/GenericRepository.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/GenericRepository.cs @@ -51,22 +51,22 @@ public IAsyncAction CloneRepositoryAsync(string cloneDestination, IDeveloperId d } catch (RecurseSubmodulesException recurseException) { - _log.Error("Could not clone all sub modules", recurseException); + _log.Error(recurseException, "Could not clone all sub modules"); throw; } catch (UserCancelledException userCancelledException) { - _log.Error("The user stoped the clone operation", userCancelledException); + _log.Error(userCancelledException, "The user stoped the clone operation"); throw; } catch (NameConflictException nameConflictException) { - _log.Error(string.Empty, nameConflictException); + _log.Error(nameConflictException, nameConflictException.ToString()); throw; } catch (Exception e) { - _log.Error("Could not clone the repository", e); + _log.Error(e, "Could not clone the repository"); throw; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs index 96052be67f..718ee3802b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs @@ -153,7 +153,7 @@ IAsyncOperation ISetupTask.Execute() catch (Exception e) { ReportAppInstallFailedEvent(); - _log.Error($"Exception thrown while installing package.", e); + _log.Error(e, $"Exception thrown while installing package."); return TaskFinishedState.Failure; } }).AsAsyncOperation(); @@ -189,7 +189,7 @@ IAsyncOperation ISetupTask.ExecuteAsAdmin(IElevatedComponentO catch (Exception e) { ReportAppInstallFailedEvent(); - _log.Error($"Exception thrown while installing package.", e); + _log.Error(e, $"Exception thrown while installing package."); return TaskFinishedState.Failure; } }).AsAsyncOperation(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs index dc47ac7579..71a414dae1 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs @@ -77,7 +77,7 @@ public void StartIfNotRunning() } catch (Exception ex) { - _log.Error($"Could not get repository provider from extension.", ex); + _log.Error(ex, $"Could not get repository provider from extension."); } } @@ -199,7 +199,7 @@ public async Task GetLoginUiAsync() } catch (Exception ex) { - _log.Error($"ShowLoginUIAsync(): loginUIContentDialog failed.", ex); + _log.Error(ex, $"ShowLoginUIAsync(): loginUIContentDialog failed."); } return null; @@ -224,7 +224,7 @@ public IEnumerable GetAllLoggedInAccounts() var developerIdsResult = _devIdProvider.GetLoggedInDeveloperIds(); if (developerIdsResult.Result.Status != ProviderOperationStatus.Success) { - _log.Error($"Could not get logged in accounts. Message: {developerIdsResult.Result.DisplayMessage}", developerIdsResult.Result.ExtendedError); + _log.Error(developerIdsResult.Result.ExtendedError, $"Could not get logged in accounts. Message: {developerIdsResult.Result.DisplayMessage}"); return new List(); } @@ -251,7 +251,7 @@ public RepositorySearchInformation SearchForRepositories(IDeveloperId developerI } else { - _log.Error($"Could not get repositories. Message: {result.Result.DisplayMessage}", result.Result.ExtendedError); + _log.Error(result.Result.ExtendedError, $"Could not get repositories. Message: {result.Result.DisplayMessage}"); } } else @@ -264,7 +264,7 @@ public RepositorySearchInformation SearchForRepositories(IDeveloperId developerI } else { - _log.Error($"Could not get repositories. Message: {result.Result.DisplayMessage}", result.Result.ExtendedError); + _log.Error(result.Result.ExtendedError, $"Could not get repositories. Message: {result.Result.DisplayMessage}"); } } } @@ -282,7 +282,7 @@ public RepositorySearchInformation SearchForRepositories(IDeveloperId developerI } catch (Exception ex) { - _log.Error($"Could not get repositories. Message: {ex}"); + _log.Error(ex, $"Could not get repositories. Message: {ex}"); } _repositories[developerId] = repoSearchInformation.Repositories; @@ -304,7 +304,7 @@ public RepositorySearchInformation GetAllRepositories(IDeveloperId developerId) } else { - _log.Error($"Could not get repositories. Message: {result.Result.DisplayMessage}", result.Result.ExtendedError); + _log.Error(result.Result.ExtendedError, $"Could not get repositories. Message: {result.Result.DisplayMessage}"); } } catch (AggregateException aggregateException) @@ -316,12 +316,12 @@ public RepositorySearchInformation GetAllRepositories(IDeveloperId developerId) } else { - _log.Error(aggregateException.Message, aggregateException); + _log.Error(aggregateException, aggregateException.Message); } } catch (Exception ex) { - _log.Error($"Could not get repositories. Message: {ex}", ex); + _log.Error(ex, $"Could not get repositories. Message: {ex}"); } _repositories[developerId] = repoSearchInformation.Repositories; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs index 9da80343a6..f74b337de5 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs @@ -133,7 +133,7 @@ private string FindVersion(IReadOnlyList availableVersions, Pa } catch (Exception e) { - _log.Error($"Unable to validate if the version {versionInfo.Version} is in the list of available versions", e); + _log.Error(e, $"Unable to validate if the version {versionInfo.Version} is in the list of available versions"); } return versionInfo.Version; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/SDKOpenConfigurationSetResult.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/SDKOpenConfigurationSetResult.cs index 3b2de8ab81..cabbf87556 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/SDKOpenConfigurationSetResult.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/SDKOpenConfigurationSetResult.cs @@ -49,7 +49,7 @@ public SDKOpenConfigurationSetResult(SDK.OpenConfigurationSetResult result, ISet public string GetErrorMessage() { var log = Log.ForContext("SourceContext", nameof(SDKOpenConfigurationSetResult)); - log.Error($"Extension failed to open the configuration file provided by Dev Home: Field: {Field}, Value: {Value}, Line: {Line}, Column: {Column}", ResultCode); + log.Error(ResultCode, $"Extension failed to open the configuration file provided by Dev Home: Field: {Field}, Value: {Value}, Line: {Line}, Column: {Column}"); return _setupFlowStringResource.GetLocalized(StringResourceKey.SetupTargetConfigurationOpenConfigFailed); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/AppManagementInitializer.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/AppManagementInitializer.cs index ca3c3f70b3..690bfb7f3c 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/AppManagementInitializer.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/AppManagementInitializer.cs @@ -74,7 +74,7 @@ private async Task InitializeWindowsPackageManagerAsync() } catch (Exception e) { - _log.Error($"Unable to correctly initialize app management at the moment. Further attempts will be performed later.", e); + _log.Error(e, $"Unable to correctly initialize app management at the moment. Further attempts will be performed later."); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/CatalogDataSourceLoader.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/CatalogDataSourceLoader.cs index 6ab0f24ab8..26664c6fc2 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/CatalogDataSourceLoader.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/CatalogDataSourceLoader.cs @@ -72,7 +72,7 @@ private async Task InitializeDataSourceAsync(WinGetPackageDataSource dataSource) } catch (Exception e) { - _log.Error($"Exception thrown while initializing data source of type {dataSource.GetType().Name}", e); + _log.Error(e, $"Exception thrown while initializing data source of type {dataSource.GetType().Name}"); } } @@ -89,7 +89,7 @@ private async Task> LoadCatalogsFromDataSourceAsync(WinGet } catch (Exception e) { - _log.Error($"Exception thrown while loading data source of type {dataSource.GetType().Name}", e); + _log.Error(e, $"Exception thrown while loading data source of type {dataSource.GetType().Name}"); } return null; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/ComputeSystemViewModelFactory.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/ComputeSystemViewModelFactory.cs index 9efdd3a9e7..ab4936a111 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/ComputeSystemViewModelFactory.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/ComputeSystemViewModelFactory.cs @@ -37,7 +37,7 @@ public async Task CreateCardViewModelAsync( catch (Exception ex) { var log = Log.ForContext("SourceContext", nameof(ComputeSystemViewModelFactory)); - log.Error($"Failed to get initial properties for compute system {computeSystem}. Error: {ex.Message}"); + log.Error(ex, $"Failed to get initial properties for compute system {computeSystem}. Error: {ex.Message}"); } return cardViewModel; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs index 21c498c6d4..4059509583 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs @@ -138,7 +138,7 @@ private List GetResourcesForCloneTaskGroup(RepoConfigTaskG } catch (Exception e) { - _log.Error($"Error creating a repository resource entry", e); + _log.Error(e, $"Error creating a repository resource entry"); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/DevDriveManager.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/DevDriveManager.cs index 1663c249bc..d4b0dbeed3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/DevDriveManager.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/DevDriveManager.cs @@ -251,7 +251,7 @@ volumeLetter is char newLetter && volumeSize is ulong newSize && catch (Exception ex) { // Log then return empty list, don't show the user their existing dev drive. Not catastrophic failure. - _log.Error($"Failed to get existing Dev Drives.", ex); + _log.Error(ex, $"Failed to get existing Dev Drives."); return new List(); } } @@ -280,7 +280,7 @@ private DevDrive GetDevDriveWithDefaultInfo() } catch (Exception ex) { - _log.Error($"Unable to get available Free Space for {root}.", ex); + _log.Error(ex, $"Unable to get available Free Space for {root}."); validationSuccessful = false; } @@ -372,7 +372,7 @@ public ISet GetDevDriveValidationResults(IDevDrive dev } catch (Exception ex) { - _log.Error($"Failed to validate selected Drive letter ({devDrive.DriveLocation.FirstOrDefault()}).", ex); + _log.Error(ex, $"Failed to validate selected Drive letter ({devDrive.DriveLocation.FirstOrDefault()})."); returnSet.Add(DevDriveValidationResult.DriveLetterNotAvailable); } @@ -405,7 +405,7 @@ public IList GetAvailableDriveLetters(char? usedLetterToKeepInList = null) } catch (Exception ex) { - _log.Error($"Failed to get Available Drive letters.", ex); + _log.Error(ex, $"Failed to get Available Drive letters."); } return driveLetterSet.ToList(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetCatalogConnector.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetCatalogConnector.cs index f195240367..d70f08539d 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetCatalogConnector.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetCatalogConnector.cs @@ -223,7 +223,7 @@ private async Task CreateAndConnectSearchCatalogAsync() } catch (Exception e) { - _log.Error($"Failed to create or connect to search catalog.", e); + _log.Error(e, $"Failed to create or connect to search catalog."); } } @@ -241,7 +241,7 @@ private async Task CreateAndConnectWinGetCatalogAsync() } catch (Exception e) { - _log.Error($"Failed to create or connect to 'winget' catalog source.", e); + _log.Error(e, $"Failed to create or connect to 'winget' catalog source."); } } @@ -259,7 +259,7 @@ private async Task CreateAndConnectMsStoreCatalogAsync() } catch (Exception e) { - _log.Error($"Failed to create or connect to 'msstore' catalog source.", e); + _log.Error(e, $"Failed to create or connect to 'msstore' catalog source."); } } @@ -278,7 +278,7 @@ private async Task CreateAndConnectCustomCatalogAsync(string cata } catch (Exception e) { - _log.Error($"Failed to create or connect to custom catalog with name {catalogName}", e); + _log.Error(e, $"Failed to create or connect to custom catalog with name {catalogName}"); return null; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetDeployment.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetDeployment.cs index ca212e18b2..8511c9fa81 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetDeployment.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetDeployment.cs @@ -53,7 +53,7 @@ await Task.Run(() => } catch (Exception e) { - _log.Error($"Failed to create dummy {nameof(PackageManager)} COM object. WinGet COM Server is not available.", e); + _log.Error(e, $"Failed to create dummy {nameof(PackageManager)} COM object. WinGet COM Server is not available."); return false; } } @@ -70,7 +70,7 @@ public async Task IsUpdateAvailableAsync() } catch (Exception e) { - _log.Error("Failed to check if AppInstaller has an update, defaulting to false", e); + _log.Error(e, "Failed to check if AppInstaller has an update, defaulting to false"); return false; } } @@ -87,12 +87,12 @@ public async Task RegisterAppInstallerAsync() } catch (RegisterPackageException e) { - _log.Error($"Failed to register AppInstaller", e); + _log.Error(e, $"Failed to register AppInstaller"); return false; } catch (Exception e) { - _log.Error("An unexpected error occurred when registering AppInstaller", e); + _log.Error(e, "An unexpected error occurred when registering AppInstaller"); return false; } } @@ -106,7 +106,7 @@ public async Task IsConfigurationUnstubbedAsync() } catch (Exception e) { - _log.Error("An unexpected error occurred when checking if configuration is unstubbed", e); + _log.Error(e, "An unexpected error occurred when checking if configuration is unstubbed"); return false; } } @@ -123,7 +123,7 @@ public async Task UnstubConfigurationAsync() } catch (Exception e) { - _log.Error("An unexpected error occurred when unstubbing configuration", e); + _log.Error(e, "An unexpected error occurred when unstubbing configuration"); return false; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetRecovery.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetRecovery.cs index c4a1006d6c..1212b1d7ec 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetRecovery.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetRecovery.cs @@ -45,12 +45,12 @@ public async Task DoWithRecoveryAsync(Func> actionFunc) } catch (CatalogNotInitializedException e) { - _log.Error($"Catalog used by the action is not initialized", e); + _log.Error(e, $"Catalog used by the action is not initialized"); await RecoveryAsync(attempt); } catch (COMException e) when (e.HResult == RpcServerUnavailable || e.HResult == RpcCallFailed || e.HResult == PackageUpdating) { - _log.Error($"Failed to operate on out-of-proc object with error code: 0x{e.HResult:x}", e); + _log.Error(e, $"Failed to operate on out-of-proc object with error code: 0x{e.HResult:x}"); await RecoveryAsync(attempt); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs index a4c8819ced..9209e267af 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs @@ -90,7 +90,7 @@ await ForEachEnabledExtensionAsync(async (extensionGroups) => } catch (Exception e) { - _log.Error($"Error loading packages from featured applications group.", e); + _log.Error(e, $"Error loading packages from featured applications group."); } } }); @@ -197,7 +197,7 @@ private async Task ForEachEnabledExtensionAsync(Func LoadCatalogAsync(JsonWinGetPackageCatalog jso } catch (Exception e) { - _log.Error($"Error loading packages from winget catalog.", e); + _log.Error(e, $"Error loading packages from winget catalog."); } return null; @@ -184,7 +184,7 @@ private async Task GetJsonApplicationIconAsync(JsonWinGetPa } catch (Exception e) { - _log.Error($"Failed to get icon for JSON package {package.Uri}.", e); + _log.Error(e, $"Failed to get icon for JSON package {package.Uri}."); } _log.Warning($"No icon found for JSON package {package.Uri}. A default one will be provided."); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs index d87fe3126a..fa9bb0b060 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs @@ -97,7 +97,7 @@ public async override Task> LoadCatalogsAsync() } catch (Exception e) { - _log.Error($"Error loading packages from winget restore catalog.", e); + _log.Error(e, $"Error loading packages from winget restore catalog."); } return result; @@ -130,7 +130,7 @@ private async Task GetRestoreApplicationIconAsync(IRestoreA } catch (Exception e) { - _log.Error($"Failed to get icon for restore package {appInfo.Id}", e); + _log.Error(e, $"Failed to get icon for restore package {appInfo.Id}"); } _log.Warning($"No {theme} icon found for restore package {appInfo.Id}. A default one will be provided."); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Utilities/DevDriveUtil.cs b/tools/SetupFlow/DevHome.SetupFlow/Utilities/DevDriveUtil.cs index 99f6c12de2..474a5c1c5c 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Utilities/DevDriveUtil.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Utilities/DevDriveUtil.cs @@ -104,7 +104,7 @@ public static bool IsDevDriveFeatureEnabled } catch (Exception ex) { - Log.Error($"Unable to query for Dev Drive enablement: {ex.Message}"); + Log.Error(ex, $"Unable to query for Dev Drive enablement: {ex.Message}"); return false; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs index b269b1be81..cd0a40d2ea 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs @@ -1051,7 +1051,7 @@ private void ValidateUriAndChangeUiIfBad(string url, out Uri uri) } catch (Exception e) { - _log.Error($"Invalid URL {uri.OriginalString}", e); + _log.Error(e, $"Invalid URL {uri.OriginalString}"); UrlParsingError = _stringResource.GetLocalized(StringResourceKey.UrlValidationBadUrl); ShouldShowUrlError = true; return; @@ -1248,7 +1248,7 @@ private async Task InitiateAddAccountUserExperienceAsync(RepositoryProvider prov } catch (Exception ex) { - _log.Error($"Exception thrown while calling show logon session", ex); + _log.Error(ex, $"Exception thrown while calling show logon session"); } } } @@ -1335,7 +1335,7 @@ private async Task CoordinateTasks(string loginId, Task PickConfigurationFileAsync() } catch (Exception e) { - _log.Error($"Failed to open file picker.", e); + _log.Error(e, $"Failed to open file picker."); return false; } } @@ -184,7 +184,7 @@ private async Task LoadConfigurationFileInternalAsync(StorageFile file) } catch (OpenConfigurationSetException e) { - _log.Error($"Opening configuration set failed.", e); + _log.Error(e, $"Opening configuration set failed."); await _mainWindow.ShowErrorMessageDialogAsync( StringResource.GetLocalized(StringResourceKey.ConfigurationViewTitle, file.Name), GetErrorMessage(e), @@ -192,7 +192,7 @@ await _mainWindow.ShowErrorMessageDialogAsync( } catch (Exception e) { - _log.Error($"Unknown error while opening configuration set.", e); + _log.Error(e, $"Unknown error while opening configuration set."); await _mainWindow.ShowErrorMessageDialogAsync( file.Name, diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DevDriveViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DevDriveViewModel.cs index 48cc57a038..8f2162075b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DevDriveViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DevDriveViewModel.cs @@ -269,7 +269,7 @@ public async Task ChooseFolderLocationAsync() } catch (Exception e) { - _log.Error("Failed to open folder picker.", e); + _log.Error(e, "Failed to open folder picker."); } } @@ -509,7 +509,7 @@ private void RefreshDriveLetterToSizeMapping() } catch (Exception ex) { - _log.Error($"Failed to refresh the drive letter to size mapping.", ex); + _log.Error(ex, $"Failed to refresh the drive letter to size mapping."); // Clear the mapping since it can't be refreshed. This shouldn't happen unless DriveInfo.GetDrives() fails. In that case we won't know which drive // in the list is causing GetDrives()'s to throw. If there are values inside the dictionary at this point, they could be stale. Clearing the list diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemsListViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemsListViewModel.cs index a50b4f1391..123d843062 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemsListViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemsListViewModel.cs @@ -146,7 +146,7 @@ public void FilterComputeSystemCards(string text) } catch (Exception ex) { - _log.Error($"Failed to filter Compute system cards. Error: {ex.Message}"); + _log.Error(ex, $"Failed to filter Compute system cards. Error: {ex.Message}"); } return true; diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs index 38fa6699a3..4a5f1a3115 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs @@ -181,7 +181,7 @@ public void UpdateExtensionAdaptiveCard(ComputeSystemAdaptiveCardResult adaptive } catch (Exception ex) { - _log.Error($"Failed to get creation options adaptive card from provider {_curProviderDetails.ComputeSystemProvider.Id}.", ex); + _log.Error(ex, $"Failed to get creation options adaptive card from provider {_curProviderDetails.ComputeSystemProvider.Id}."); SessionErrorMessage = ex.Message; } }); diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs index 9d7999dfd9..7300d8bc9c 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs @@ -146,7 +146,7 @@ private async Task PickCloneDirectoryAsync() } catch (Exception e) { - _log.Error("Failed to open folder picker", e); + _log.Error(e, "Failed to open folder picker"); return null; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs index 693271087e..f8728f3722 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs @@ -93,7 +93,7 @@ private async Task LoadCatalogsAsync() } catch (Exception e) { - _log.Error($"Failed to load catalogs.", e); + _log.Error(e, $"Failed to load catalogs."); } finally { diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs index 1129c1b62f..8926e1b559 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs @@ -164,7 +164,7 @@ private async Task OnSetUpAsync() } catch (Exception e) { - _log.Error($"Failed to initialize elevated process.", e); + _log.Error(e, $"Failed to initialize elevated process."); } } @@ -188,7 +188,7 @@ private async Task DownloadConfigurationAsync() } catch (Exception e) { - _log.Error($"Failed to download configuration file.", e); + _log.Error(e, $"Failed to download configuration file."); } } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs index 5cb8bfd161..a80727ff66 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs @@ -128,7 +128,7 @@ public SearchViewModel(IWindowsPackageManager wpm, ISetupFlowStringResource stri } catch (Exception e) { - _log.Error($"Search error.", e); + _log.Error(e, $"Search error."); return (SearchResultStatus.ExceptionThrown, null); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs index 44ed8eabbe..80078db0f2 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs @@ -196,7 +196,7 @@ private bool ShouldShowListInUI(ComputeSystemsListViewModel listViewModel, strin } catch (Exception ex) { - _log.Error($"Error filtering ComputeSystemsListViewModel", ex); + _log.Error(ex, $"Error filtering ComputeSystemsListViewModel"); } return true; @@ -366,7 +366,7 @@ public async Task LoadAllComputeSystemsInTheUI() } catch (Exception ex) { - _log.Error($"Error loading ComputeSystemViewModels data", ex); + _log.Error(ex, $"Error loading ComputeSystemViewModels data"); } ShouldShowShimmerBelowList = false; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs index c1532b9b71..24ca943ebb 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs @@ -74,7 +74,7 @@ private void AddAdaptiveCardToUI(RenderedAdaptiveCard renderedAdaptiveCard) catch (Exception ex) { // Log the exception - _log.Error("Error adding adaptive card UI in CreateEnvironmentReviewView", ex); + _log.Error(ex, "Error adding adaptive card UI in CreateEnvironmentReviewView"); } } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs index 3bb7562cc0..7a438b6ae8 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs @@ -72,7 +72,7 @@ private void AddAdaptiveCardToUI(RenderedAdaptiveCard adaptiveCardData) catch (Exception ex) { // Log the exception - _log.Error("Error adding adaptive card UI in EnvironmentCreationOptionsView", ex); + _log.Error(ex, "Error adding adaptive card UI in EnvironmentCreationOptionsView"); } } } From f8e0dc407c1c9ea9b8b1b05f12c004956b409573 Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Wed, 10 Apr 2024 15:38:42 -0400 Subject: [PATCH 083/121] Handle a missing PropertySet gracefully (#2579) --- src/Package.appxmanifest | 2 +- src/Services/ExtensionService.cs | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Package.appxmanifest b/src/Package.appxmanifest index f6308beed4..3e44bb0bc3 100644 --- a/src/Package.appxmanifest +++ b/src/Package.appxmanifest @@ -86,7 +86,7 @@ - + diff --git a/src/Services/ExtensionService.cs b/src/Services/ExtensionService.cs index 0566a581bf..032d45c2cf 100644 --- a/src/Services/ExtensionService.cs +++ b/src/Services/ExtensionService.cs @@ -313,12 +313,12 @@ protected virtual void Dispose(bool disposing) private IPropertySet? GetSubPropertySet(IPropertySet propSet, string name) { - return propSet[name] as IPropertySet; + return propSet.TryGetValue(name, out var value) ? value as IPropertySet : null; } private object[]? GetSubPropertySetArray(IPropertySet propSet, string name) { - return propSet[name] as object[]; + return propSet.TryGetValue(name, out var value) ? value as object[] : null; } /// @@ -330,7 +330,6 @@ private List GetCreateInstanceList(IPropertySet activationPropSet) { var propSetList = new List(); var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty); - var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty); if (singlePropertySet != null) { var classId = GetProperty(singlePropertySet, ClassIdProperty); @@ -341,19 +340,23 @@ private List GetCreateInstanceList(IPropertySet activationPropSet) propSetList.Add(classId); } } - else if (propertySetArray != null) + else { - foreach (var prop in propertySetArray) + var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty); + if (propertySetArray != null) { - if (prop is not IPropertySet propertySet) + foreach (var prop in propertySetArray) { - continue; - } + if (prop is not IPropertySet propertySet) + { + continue; + } - var classId = GetProperty(propertySet, ClassIdProperty); - if (classId != null) - { - propSetList.Add(classId); + var classId = GetProperty(propertySet, ClassIdProperty); + if (classId != null) + { + propSetList.Add(classId); + } } } } From ebfbe697de606c651b83af5852b6a0d9a7ea6b46 Mon Sep 17 00:00:00 2001 From: Darren Hoehna Date: Wed, 10 Apr 2024 12:44:45 -0700 Subject: [PATCH 084/121] Adding tooltip to config buttons (#2559) * Making the name better * Adding tooltip and moving code out of code-behind * Using path.join: * Removin unused usings --------- Co-authored-by: Darren Hoehna --- .../ViewModels/AddRepoViewModel.cs | 27 +++++++++++++++++-- .../ViewModels/FolderPickerViewModel.cs | 1 - .../Views/AddRepoDialog.xaml | 20 +++++++++++--- .../Views/AddRepoDialog.xaml.cs | 23 ---------------- .../Views/RepoConfigView.xaml | 16 +++++++++-- 5 files changed, 55 insertions(+), 32 deletions(-) diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs index cd0a40d2ea..636f80d46e 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -339,6 +340,28 @@ public async Task ShowCustomizeDevDriveWindow() ToggleCloneButton(); } + [RelayCommand] + public void DevDriveCloneLocationChanged() + { + var location = (EditDevDriveViewModel.DevDrive != null) ? EditDevDriveViewModel.GetDriveDisplayName() : string.Empty; + + if (!string.IsNullOrEmpty(location)) + { + SaveCloneLocation(location); + } + } + + [RelayCommand] + public void SaveCloneLocation(string location) + { + // In cases where location is empty don't update the cloneLocation. Only update when there are actual values. + FolderPickerViewModel.CloneLocation = location; + + FolderPickerViewModel.ValidateCloneLocation(); + + ToggleCloneButton(); + } + /// /// Indicates if the ListView is currently filtering items. A result of manually filtering a list view /// is that the SelectionChanged is fired for any selected item that is removed and the item isn't "re-selected" @@ -1014,8 +1037,8 @@ public void AddOrRemoveRepository(string accountName, IList repositories cloningInformation.RepositoryProvider = _providers.GetSDKProvider(_selectedRepoProvider); cloningInformation.ProviderName = _providers.DisplayName(_selectedRepoProvider); cloningInformation.OwningAccount = developerId; - cloningInformation.EditClonePathAutomationName = _stringResource.GetLocalized(StringResourceKey.RepoPageEditClonePathAutomationProperties, $"{_selectedRepoProvider}/{repositoryToAdd}"); - cloningInformation.RemoveFromCloningAutomationName = _stringResource.GetLocalized(StringResourceKey.RepoPageRemoveRepoAutomationProperties, $"{_selectedRepoProvider}/{repositoryToAdd}"); + cloningInformation.EditClonePathAutomationName = _stringResource.GetLocalized(StringResourceKey.RepoPageEditClonePathAutomationProperties, Path.Join(_selectedRepoProvider, repositoryToAdd.RepoDisplayName)); + cloningInformation.RemoveFromCloningAutomationName = _stringResource.GetLocalized(StringResourceKey.RepoPageRemoveRepoAutomationProperties, Path.Join(_selectedRepoProvider, repositoryToAdd.RepoDisplayName)); EverythingToClone.Add(cloningInformation); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs index 7300d8bc9c..f4e5b397c9 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs @@ -10,7 +10,6 @@ using DevHome.SetupFlow.Services; using Microsoft.UI.Xaml; using Serilog; -using Windows.Storage.Pickers; using WinUIEx; namespace DevHome.SetupFlow.ViewModels; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml index 06639efbad..89422e4985 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml @@ -211,17 +211,29 @@ + x:Name="DevDriveCloneLocationAliasTextBox"> + + + + + + + Visibility="{x:Bind AddRepoViewModel.FolderPickerViewModel.InDevDriveScenario, Mode=OneWay, Converter={StaticResource NegatedBoolToVisibilityConverter}}"> + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml.cs index b45115b4f1..428b5fddeb 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml.cs @@ -110,29 +110,6 @@ public void DeveloperIdChangedEventHandler(object sender, IDeveloperId developer } } - /// - /// Validate the user put in a rooted, non-null path. - /// - private void CloneLocation_TextChanged(object sender, TextChangedEventArgs e) - { - // just in case something other than a text box calls this. - if (sender is TextBox cloneLocationTextBox) - { - var location = cloneLocationTextBox.Text; - if (string.Equals(cloneLocationTextBox.Name, "DevDriveCloneLocationAliasTextBox", StringComparison.Ordinal)) - { - location = (AddRepoViewModel.EditDevDriveViewModel.DevDrive != null) ? AddRepoViewModel.EditDevDriveViewModel.GetDriveDisplayName() : string.Empty; - } - - // In cases where location is empty don't update the cloneLocation. Only update when there are actual values. - AddRepoViewModel.FolderPickerViewModel.CloneLocation = string.IsNullOrEmpty(location) ? AddRepoViewModel.FolderPickerViewModel.CloneLocation : location; - } - - AddRepoViewModel.FolderPickerViewModel.ValidateCloneLocation(); - - AddRepoViewModel.ToggleCloneButton(); - } - /// /// If any items in reposToSelect exist in the UI, select them. /// An side-effect of SelectRange is SelectionChanged is fired for each item SelectRange is called on. diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml index bc2c11c908..a86acf64c6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml @@ -146,14 +146,26 @@ VerticalAlignment="Center"/> - - public bool RequiresReboot => false; + /// + /// Gets target device name. Inherited via ISetupTask but unused. + /// + public string TargetName => string.Empty; + /// /// The developer ID that is used when a repository is being cloned. /// diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs index c97ca72986..261ac847ce 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs @@ -16,8 +16,8 @@ using DevHome.SetupFlow.Common.Exceptions; using DevHome.SetupFlow.Exceptions; using DevHome.SetupFlow.Models.WingetConfigure; -using DevHome.SetupFlow.Services; -using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.Services; +using DevHome.SetupFlow.ViewModels; using Microsoft.UI.Xaml; using Microsoft.Windows.DevHome.SDK; using Projection::DevHome.SetupFlow.ElevatedComponent; @@ -50,6 +50,11 @@ public class ConfigureTargetTask : ISetupTask // Inherited via ISetupTask but unused public bool RequiresReboot => false; + // Inherited via ISetupTask + public string TargetName => string.IsNullOrEmpty(ComputeSystemName) ? + _stringResource.GetLocalized(StringResourceKey.SetupTargetMachineName) : + ComputeSystemName; + // Inherited via ISetupTask but unused public bool DependsOnDevDriveToBeInstalled => false; @@ -61,7 +66,7 @@ public class ConfigureTargetTask : ISetupTask public ActionCenterMessages ActionCenterMessages { get; set; } = new() { ExtensionAdaptiveCardPanel = new(), }; - public string ComputeSystemName { get; private set; } = string.Empty; + public string ComputeSystemName => _computeSystemManager.ComputeSystemSetupItem.ComputeSystemToSetup.DisplayName ?? string.Empty; public SDK.IExtensionAdaptiveCardSession2 ExtensionAdaptiveCardSession { get; private set; } @@ -84,9 +89,9 @@ public class ConfigureTargetTask : ISetupTask public SDKApplyConfigurationResult Result { get; private set; } public IAsyncOperation ApplyConfigurationAsyncOperation { get; private set; } - + public ISummaryInformationViewModel SummaryScreenInformation { get; } - + public ConfigureTargetTask( ISetupFlowStringResource stringResource, IComputeSystemManager computeSystemManager, @@ -147,8 +152,8 @@ public void OnActionRequired(IApplyConfigurationOperation operation, SDK.ApplyCo ExtensionAdaptiveCardSession.Stopped += OnAdaptiveCardSessionStopped; CreateCorrectiveActionPanel(ExtensionAdaptiveCardSession).GetAwaiter().GetResult(); - - AddMessage(_stringResource.GetLocalized(StringResourceKey.ConfigureTargetApplyConfigurationActionNeeded, UserNumberOfAttempts++, UserMaxNumberOfAttempts), MessageSeverityKind.Warning); + + AddMessage(_stringResource.GetLocalized(StringResourceKey.ConfigureTargetApplyConfigurationActionNeeded, UserNumberOfAttempts++, UserMaxNumberOfAttempts), MessageSeverityKind.Warning); } else { @@ -174,7 +179,6 @@ public void OnApplyConfigurationOperationChanged(object sender, SDK.Configuratio var wrapper = new SDKConfigurationSetChangeWrapper(progressData, _stringResource); var potentialErrorMsg = wrapper.GetErrorMessagesForDisplay(); var stringBuilder = new StringBuilder(); - stringBuilder.AppendLine("---- " + _stringResource.GetLocalized(StringResourceKey.SetupTargetConfigurationProgressUpdate) + " ----"); var startingLineNumber = 0u; if (wrapper.Change == SDK.ConfigurationSetChangeEventType.SetStateChanged) @@ -195,22 +199,46 @@ public void OnApplyConfigurationOperationChanged(object sender, SDK.Configuratio // there is no way for us to know what the extension is doing, it may not have started configuration yet but may simply be installing prerequisites. if (wrapper.Unit != null) { - // We may need to change the formatting of the message in the future. - var description = BuildConfigurationUnitDescription(wrapper.Unit); - stringBuilder.AppendLine(GetSpacingForProgressMessage(startingLineNumber++) + description); - stringBuilder.AppendLine(GetSpacingForProgressMessage(startingLineNumber++) + wrapper.ConfigurationUnitState); + // Showing "pending" unit states is not useful to the user, so we'll ignore them. + if (wrapper.UnitState != ConfigurationUnitState.Pending) + { + var description = BuildConfigurationUnitDescription(wrapper.Unit); + stringBuilder.AppendLine(description.packageIdDescription); + if (!string.IsNullOrEmpty(description.packageNameDescription)) + { + stringBuilder.AppendLine(description.packageNameDescription); + } + + stringBuilder.AppendLine(wrapper.ConfigurationUnitState); + if ((wrapper.UnitState == ConfigurationUnitState.Completed) && !wrapper.IsErrorMessagePresent) + { + severity = MessageSeverityKind.Success; + } + } + else + { + _log.Information("Ignoring configuration unit pending state."); + } } else { _log.Information("Extension sent progress but there was no configuration unit data sent."); } - // Example of a message that will be displayed in the UI: - // ---- Configuration progress received! ---- - // There was an issue applying part of the configuration using DSC resource: 'GitClone'.Check the extension's logs - // - Assert : GitClone[Clone: wil - C:\Users\Public\Documents\source\repos\wil] - // - This part of the configuration is now complete - AddMessage(stringBuilder.ToString(), severity); + // Examples of a message that will be displayed in the UI: + // Apply: WinGetPackage [Microsoft.VisualStudioCode] + // Install: Microsoft Visual Studio Code + // Configuration applied + // + // There was an issue applying part of the configuration using DSC resource: 'WinGetPackage'.Error: WinGetPackage Failed installing Notepad++.Notepad++. + // InstallStatus 'InstallError' InstallerErrorCode '0' ExtendedError '-2147023673' + // Apply: WinGetPackage[Notepad++.Notepad++] + // Install: Notepad++ + // Configuration applied + if (stringBuilder.Length > 0) + { + AddMessage(stringBuilder.ToString(), severity); + } } catch (Exception ex) { @@ -265,12 +293,6 @@ public void HandleCompletedOperation(SDK.ApplyConfigurationResult applyConfigura throw new OpenConfigurationSetException(Result.OpenResult.ResultCode, Result.OpenResult.Field, Result.OpenResult.Value); } - // Check if the WinGet apply operation was failed. - if (!Result.ApplyConfigSucceeded) - { - throw new SDKApplyConfigurationSetResultException("Unable to get the result of the apply configuration set as it was null."); - } - // Gather the configuration results. We'll display these to the user in the summary page if they are available. if (Result.ApplyResult.AreConfigUnitsAvailable) { @@ -283,6 +305,19 @@ public void HandleCompletedOperation(SDK.ApplyConfigurationResult applyConfigura } else { + // Check if the WinGet apply operation failed. + if (Result.ApplyResult.ResultException != null) + { + // TODO: We should propagate this error to Summery page. + throw Result.ApplyResult.ResultException; + } + else if (!Result.ApplyConfigSucceeded) + { + // Failed, but no configuration units and no result exception. Something is wrong with result reporting. + throw new SDKApplyConfigurationSetResultException("Unable to get the result of the apply configuration set as it was null."); + } + + // Succeeded, but no configuration units. Something is wrong with result reporting. throw new SDKApplyConfigurationSetResultException("No configuration units were found. This is likely due to an error within the extension."); } } @@ -325,10 +360,9 @@ public IAsyncOperation Execute() try { UserNumberOfAttempts = 1; - var computeSystem = _computeSystemManager.ComputeSystemSetupItem.ComputeSystemToSetup; - ComputeSystemName = computeSystem.DisplayName; AddMessage(_stringResource.GetLocalized(StringResourceKey.SetupTargetExtensionApplyingConfiguration, ComputeSystemName), MessageSeverityKind.Info); WingetConfigFileString = _configurationFileBuilder.BuildConfigFileStringFromTaskGroups(_setupFlowOrchestrator.TaskGroups, ConfigurationFileKind.SetupTarget); + var computeSystem = _computeSystemManager.ComputeSystemSetupItem.ComputeSystemToSetup; var applyConfigurationOperation = computeSystem.ApplyConfiguration(WingetConfigFileString); applyConfigurationOperation.ConfigurationSetStateChanged += OnApplyConfigurationOperationChanged; @@ -381,14 +415,12 @@ public IAsyncOperation Execute() TaskMessages ISetupTask.GetLoadingMessages() { - var localizedTargetName = _stringResource.GetLocalized(StringResourceKey.SetupTargetMachineName); - var nameToUseInDisplay = string.IsNullOrEmpty(ComputeSystemName) ? localizedTargetName : ComputeSystemName; return new() { - Executing = _stringResource.GetLocalized(StringResourceKey.SetupTargetExtensionApplyingConfiguration, nameToUseInDisplay), - Error = _stringResource.GetLocalized(StringResourceKey.SetupTargetExtensionApplyConfigurationError, nameToUseInDisplay), - Finished = _stringResource.GetLocalized(StringResourceKey.SetupTargetExtensionApplyConfigurationSuccess, nameToUseInDisplay), - NeedsReboot = _stringResource.GetLocalized(StringResourceKey.SetupTargetExtensionApplyConfigurationRebootRequired, nameToUseInDisplay), + Executing = _stringResource.GetLocalized(StringResourceKey.SetupTargetExtensionApplyingConfiguration, TargetName), + Error = _stringResource.GetLocalized(StringResourceKey.SetupTargetExtensionApplyConfigurationError, TargetName), + Finished = _stringResource.GetLocalized(StringResourceKey.SetupTargetExtensionApplyConfigurationSuccess, TargetName), + NeedsReboot = _stringResource.GetLocalized(StringResourceKey.SetupTargetExtensionApplyConfigurationRebootRequired, TargetName), }; } @@ -433,7 +465,7 @@ await _windowEx.DispatcherQueue.EnqueueAsync(async () => }); } - private string BuildConfigurationUnitDescription(ConfigurationUnit unit) + private (string packageIdDescription, string packageNameDescription) BuildConfigurationUnitDescription(ConfigurationUnit unit) { var unitDescription = string.Empty; @@ -444,19 +476,27 @@ private string BuildConfigurationUnitDescription(ConfigurationUnit unit) if (string.IsNullOrEmpty(unit.Identifier) && string.IsNullOrEmpty(unitDescription)) { - return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryMinimal, unit.Intent, unit.Type); + return (_stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryMinimal, unit.Intent, unit.Type), string.Empty); } if (string.IsNullOrEmpty(unit.Identifier)) { - return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryNoId, unit.Intent, unit.Type, unitDescription); + return (_stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryNoId, unit.Intent, unit.Type, unitDescription), string.Empty); + } + + var descriptionParts = unit.Identifier.Split(ConfigurationFileBuilder.PackageNameSeparator); + var packageId = descriptionParts[0]; + var packageName = string.Empty; + if (descriptionParts.Length > 1) + { + packageName = $"Install: {descriptionParts[1]}"; } if (string.IsNullOrEmpty(unitDescription)) { - return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryNoDescription, unit.Intent, unit.Type, unit.Identifier); + return (_stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryNoDescription, unit.Intent, unit.Type, packageId), packageName); } - return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryFull, unit.Intent, unit.Type, unit.Identifier, unitDescription); + return (_stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryFull, unit.Intent, unit.Type, packageId, unitDescription), packageName); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs index 353e74ba90..862216baae 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs @@ -38,6 +38,11 @@ public class ConfigureTask : ISetupTask public bool RequiresReboot { get; private set; } + /// + /// Gets target device name. Inherited via ISetupTask but unused. + /// + public string TargetName => string.Empty; + public bool DependsOnDevDriveToBeInstalled => false; public IList UnitResults diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs index bd43532b02..58fc58dab2 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs @@ -44,6 +44,11 @@ internal sealed class CreateDevDriveTask : ISetupTask public bool RequiresReboot => false; + /// + /// Gets target device name. Inherited via ISetupTask but unused. + /// + public string TargetName => string.Empty; + public bool DependsOnDevDriveToBeInstalled => false; public IDevDrive DevDrive diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs index 41d541f11e..a014dcd238 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs @@ -61,6 +61,11 @@ public sealed class CreateEnvironmentTask : ISetupTask, IDisposable, IRecipient< public bool RequiresReboot => false; + /// + /// Gets target device name. Inherited via ISetupTask but unused. + /// + public string TargetName => string.Empty; + public bool DependsOnDevDriveToBeInstalled => false; public bool CreationOperationStarted { get; private set; } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTask.cs index 7fc8c97158..5661d7f8e8 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTask.cs @@ -52,6 +52,14 @@ public bool RequiresReboot get; } + /// + /// Gets target device name. + /// + public string TargetName + { + get; + } + /// /// Executes this setup task. /// diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs index 718ee3802b..e46fb880d7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs @@ -45,6 +45,11 @@ public class InstallPackageTask : ISetupTask // installation in the WinGet COM API, but we do get it after installation. public bool RequiresReboot { get; set; } + /// + /// Gets target device name. Inherited via ISetupTask but unused. + /// + public string TargetName => string.Empty; + // May potentially be moved to a central list in the future. public bool WasInstallSuccessful { diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs index 4059509583..e5fdd02e51 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs @@ -1,52 +1,57 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using DevHome.SetupFlow.Common.Helpers; -using DevHome.SetupFlow.Models; -using DevHome.SetupFlow.Models.WingetConfigure; -using DevHome.SetupFlow.TaskGroups; -using Serilog; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace DevHome.SetupFlow.Services; - -public enum ConfigurationFileKind -{ - Normal, - SetupTarget, -} - -public class ConfigurationFileBuilder -{ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DevHome.SetupFlow.Common.Helpers; +using DevHome.SetupFlow.Models; +using DevHome.SetupFlow.Models.WingetConfigure; +using DevHome.SetupFlow.TaskGroups; +using Serilog; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace DevHome.SetupFlow.Services; + +public enum ConfigurationFileKind +{ + Normal, + SetupTarget, +} + +public class ConfigurationFileBuilder +{ + public const string PackageNameSeparator = " | "; + public const string RepoNamePrefix = "Clone "; + public const string RepoNameSuffix = ": "; + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ConfigurationFileBuilder)); - /// - /// Builds an object that represents a config file that can be used by WinGet Configure to install - /// apps and clone repositories.This is already formatted as valid yaml and can be written - /// directly to a file. - /// - /// The config file object representing the yaml file. - public WinGetConfigFile BuildConfigFileObjectFromTaskGroups(IList taskGroups, ConfigurationFileKind configurationFileKind) - { + /// + /// Builds an object that represents a config file that can be used by WinGet Configure to install + /// apps and clone repositories.This is already formatted as valid yaml and can be written + /// directly to a file. + /// + /// The config file object representing the yaml file. + public WinGetConfigFile BuildConfigFileObjectFromTaskGroups(IList taskGroups, ConfigurationFileKind configurationFileKind) + { List repoResources = []; List appResources = []; - foreach (var taskGroup in taskGroups) - { - if (taskGroup is RepoConfigTaskGroup repoConfigGroup) - { - // Add the GitDSC resource blocks to yaml - repoResources.AddRange(GetResourcesForCloneTaskGroup(repoConfigGroup, configurationFileKind)); - } - else if (taskGroup is AppManagementTaskGroup appManagementGroup) - { - // Add the WinGetDsc resource blocks to yaml - appResources.AddRange(GetResourcesForAppManagementTaskGroup(appManagementGroup, configurationFileKind)); - } - } + foreach (var taskGroup in taskGroups) + { + if (taskGroup is RepoConfigTaskGroup repoConfigGroup) + { + // Add the GitDSC resource blocks to yaml + repoResources.AddRange(GetResourcesForCloneTaskGroup(repoConfigGroup, configurationFileKind)); + } + else if (taskGroup is AppManagementTaskGroup appManagementGroup) + { + // Add the WinGetDsc resource blocks to yaml + appResources.AddRange(GetResourcesForAppManagementTaskGroup(appManagementGroup, configurationFileKind)); + } + } // If Git is not added to the apps to install and there are // repositories to clone, add Git as a pre-requisite @@ -55,80 +60,80 @@ public WinGetConfigFile BuildConfigFileObjectFromTaskGroups(IList s.Id == DscHelpers.GitWinGetPackageId); if (!isGitAdded && repoResources.Count > 0) { - appResources.Add(CreateWinGetInstallForGitPreReq()); + appResources.Add(CreateWinGetInstallForGitPreReq(configurationFileKind)); } List listOfResources = [..appResources, ..repoResources]; - - if (listOfResources.Count == 0) - { - return new WinGetConfigFile(); - } - - var wingetConfigProperties = new WinGetConfigProperties(); - - // Remove duplicate resources with the same Id but keep ordering. This is needed because the - // Id of the resource should be unique as per winget configure requirements. - listOfResources = listOfResources - .GroupBy(resource => resource.Id) - .Select(group => group.First()) - .ToList(); - - // Merge the resources into the Resources property in the properties object - wingetConfigProperties.Resources = listOfResources.ToArray(); - wingetConfigProperties.ConfigurationVersion = DscHelpers.WinGetConfigureVersion; - - // Create the new WinGetConfigFile object and serialize it to yaml - return new WinGetConfigFile() { Properties = wingetConfigProperties }; - } - - /// - /// Builds the yaml string that is used by WinGet Configure to install the apps and clone the repositories. - /// This is already formatted as valid yaml and can be written directly to a file. - /// - /// The string representing the yaml file. This string is formatted as yaml. - public string BuildConfigFileStringFromTaskGroups(IList taskGroups, ConfigurationFileKind configurationFileKind) - { - // Create the new WinGetConfigFile object and serialize it to yaml - var wingetConfigFile = BuildConfigFileObjectFromTaskGroups(taskGroups, configurationFileKind); - return SerializeWingetFileObjectToString(wingetConfigFile); - } - - /// - /// Builds the yaml string that is used by WinGet Configure to install the apps and clone the repositories. - /// This is already formatted as valid yaml and can be written directly to a file. - /// - /// The string representing the yaml file. This string is formatted as yaml. - public string SerializeWingetFileObjectToString(WinGetConfigFile configFile) - { - // Create the new WinGetConfigFile object and serialize it to yaml - var yamlSerializer = new SerializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) - .Build(); - - // Add the header banner and add two new lines after the header. - var configStringWithHeader = DscHelpers.DevHomeHeaderBanner + Environment.NewLine + Environment.NewLine; - var yaml = yamlSerializer.Serialize(configFile); - configStringWithHeader += yaml; - return configStringWithHeader; - } - - /// - /// Creates a list of WinGetConfigResource objects from the CloneRepoTask objects in the RepoConfigTaskGroup - /// - /// Clone repository task group where cloning information is located - /// List of objects that represent a WinGet configure resource block - private List GetResourcesForCloneTaskGroup(RepoConfigTaskGroup repoConfigGroup, ConfigurationFileKind configurationFileKind) - { - var listOfResources = new List(); - var repoConfigTasks = repoConfigGroup.DSCTasks - .Where(task => task is CloneRepoTask) - .Select(task => task as CloneRepoTask) - .ToList(); - - foreach (var repoConfigTask in repoConfigTasks) - { + + if (listOfResources.Count == 0) + { + return new WinGetConfigFile(); + } + + var wingetConfigProperties = new WinGetConfigProperties(); + + // Remove duplicate resources with the same Id but keep ordering. This is needed because the + // Id of the resource should be unique as per winget configure requirements. + listOfResources = listOfResources + .GroupBy(resource => resource.Id) + .Select(group => group.First()) + .ToList(); + + // Merge the resources into the Resources property in the properties object + wingetConfigProperties.Resources = listOfResources.ToArray(); + wingetConfigProperties.ConfigurationVersion = DscHelpers.WinGetConfigureVersion; + + // Create the new WinGetConfigFile object and serialize it to yaml + return new WinGetConfigFile() { Properties = wingetConfigProperties }; + } + + /// + /// Builds the yaml string that is used by WinGet Configure to install the apps and clone the repositories. + /// This is already formatted as valid yaml and can be written directly to a file. + /// + /// The string representing the yaml file. This string is formatted as yaml. + public string BuildConfigFileStringFromTaskGroups(IList taskGroups, ConfigurationFileKind configurationFileKind) + { + // Create the new WinGetConfigFile object and serialize it to yaml + var wingetConfigFile = BuildConfigFileObjectFromTaskGroups(taskGroups, configurationFileKind); + return SerializeWingetFileObjectToString(wingetConfigFile); + } + + /// + /// Builds the yaml string that is used by WinGet Configure to install the apps and clone the repositories. + /// This is already formatted as valid yaml and can be written directly to a file. + /// + /// The string representing the yaml file. This string is formatted as yaml. + public string SerializeWingetFileObjectToString(WinGetConfigFile configFile) + { + // Create the new WinGetConfigFile object and serialize it to yaml + var yamlSerializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) + .Build(); + + // Add the header banner and add two new lines after the header. + var configStringWithHeader = DscHelpers.DevHomeHeaderBanner + Environment.NewLine + Environment.NewLine; + var yaml = yamlSerializer.Serialize(configFile); + configStringWithHeader += yaml; + return configStringWithHeader; + } + + /// + /// Creates a list of WinGetConfigResource objects from the CloneRepoTask objects in the RepoConfigTaskGroup + /// + /// Clone repository task group where cloning information is located + /// List of objects that represent a WinGet configure resource block + private List GetResourcesForCloneTaskGroup(RepoConfigTaskGroup repoConfigGroup, ConfigurationFileKind configurationFileKind) + { + var listOfResources = new List(); + var repoConfigTasks = repoConfigGroup.DSCTasks + .Where(task => task is CloneRepoTask) + .Select(task => task as CloneRepoTask) + .ToList(); + + foreach (var repoConfigTask in repoConfigTasks) + { try { if (!repoConfigTask.RepositoryToClone.IsPrivate) @@ -140,107 +145,117 @@ private List GetResourcesForCloneTaskGroup(RepoConfigTaskG { _log.Error(e, $"Error creating a repository resource entry"); } - } - - return listOfResources; - } - - /// - /// Creates a list of WinGetConfigResource objects from the InstallPackageTask objects in the AppManagementTaskGroup - /// - /// The task group that holds information about the apps the user wants to install - /// List of objects that represent a WinGet configure resource block - private List GetResourcesForAppManagementTaskGroup(AppManagementTaskGroup appManagementGroup, ConfigurationFileKind configurationFileKind) - { - var listOfResources = new List(); - var installList = appManagementGroup.DSCTasks - .Where(task => task is InstallPackageTask) - .Select(task => task as InstallPackageTask) - .ToList(); - - foreach (var installTask in installList) - { - listOfResources.Add(CreateResourceFromTaskForWinGetDsc(installTask, configurationFileKind)); - } - - return listOfResources; - } - - /// - /// Creates a WinGetConfigResource object from an InstallPackageTask object. - /// - /// The install task with the package information for the app - /// The WinGetConfigResource object that represents the block of yaml needed by WinGetDsc to install the app. - private WinGetConfigResource CreateResourceFromTaskForWinGetDsc(InstallPackageTask task, ConfigurationFileKind configurationFileKind) - { - var arguments = task.GetArguments(); - var id = arguments.PackageId; - - if (configurationFileKind == ConfigurationFileKind.SetupTarget) - { - // WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI. - // So we add a description to the Id to make it more readable in the UI. These do not need to be localized. - id = $"{arguments.PackageId} | Install: " + task.PackageName; - } - - return new WinGetConfigResource() - { - Resource = DscHelpers.WinGetDscResource, - Id = id, + } + + return listOfResources; + } + + /// + /// Creates a list of WinGetConfigResource objects from the InstallPackageTask objects in the AppManagementTaskGroup + /// + /// The task group that holds information about the apps the user wants to install + /// List of objects that represent a WinGet configure resource block + private List GetResourcesForAppManagementTaskGroup(AppManagementTaskGroup appManagementGroup, ConfigurationFileKind configurationFileKind) + { + var listOfResources = new List(); + var installList = appManagementGroup.DSCTasks + .Where(task => task is InstallPackageTask) + .Select(task => task as InstallPackageTask) + .ToList(); + + foreach (var installTask in installList) + { + listOfResources.Add(CreateResourceFromTaskForWinGetDsc(installTask, configurationFileKind)); + } + + return listOfResources; + } + + /// + /// Creates a WinGetConfigResource object from an InstallPackageTask object. + /// + /// The install task with the package information for the app + /// The WinGetConfigResource object that represents the block of yaml needed by WinGetDsc to install the app. + private WinGetConfigResource CreateResourceFromTaskForWinGetDsc(InstallPackageTask task, ConfigurationFileKind configurationFileKind) + { + var arguments = task.GetArguments(); + var id = arguments.PackageId; + + if (configurationFileKind == ConfigurationFileKind.SetupTarget) + { + // WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI. + // So we add a description to the Id to make it more readable in the UI. These do not need to be localized. + id = $"{arguments.PackageId}{PackageNameSeparator}{task.PackageName}"; + } + + return new WinGetConfigResource() + { + Resource = DscHelpers.WinGetDscResource, + Id = id, Directives = new() { AllowPrerelease = true, Description = $"Installing {arguments.PackageId}", - }, + }, Settings = new WinGetDscSettings() { Id = arguments.PackageId, Source = arguments.CatalogName, - }, - }; - } - - /// - /// Creates a WinGetConfigResource object from a CloneRepoTask object. - /// - /// The task that includes the cloning information for the repository - /// The url to the public Git repository - /// The WinGetConfigResource object that represents the block of yaml needed by GitDsc to clone the repository. - private WinGetConfigResource CreateResourceFromTaskForGitDsc(CloneRepoTask task, Uri webAddress, ConfigurationFileKind configurationFileKind) - { + }, + }; + } + + /// + /// Creates a WinGetConfigResource object from a CloneRepoTask object. + /// + /// The task that includes the cloning information for the repository + /// The url to the public Git repository + /// The WinGetConfigResource object that represents the block of yaml needed by GitDsc to clone the repository. + private WinGetConfigResource CreateResourceFromTaskForGitDsc(CloneRepoTask task, Uri webAddress, ConfigurationFileKind configurationFileKind) + { // WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI. // So we add a description to the Id to make it more readable in the UI. These do not need to be localized. - var id = $"Clone {task.RepositoryName}: {task.CloneLocation.FullName}"; - var gitDependsOnId = DscHelpers.GitWinGetPackageId; - - if (configurationFileKind == ConfigurationFileKind.SetupTarget) - { - gitDependsOnId = $"{DscHelpers.GitWinGetPackageId} | Install: {DscHelpers.GitName}"; - } - - return new WinGetConfigResource() - { - Resource = DscHelpers.GitCloneDscResource, - Id = id, - Directives = new() { AllowPrerelease = true, Description = $"Cloning: {task.RepositoryName}" }, - DependsOn = [gitDependsOnId], - Settings = new GitDscSettings() { HttpsUrl = webAddress.AbsoluteUri, RootDirectory = task.CloneLocation.FullName }, - }; - } - - /// - /// Creates a WinGetConfigResource object for the GitDsc resource that installs Git for Windows. This is a pre-requisite for - /// the GitDsc resource that clones the repository. - /// - /// The WinGetConfigResource object that represents the block of yaml needed by WinGetDsc to install the Git app. - private WinGetConfigResource CreateWinGetInstallForGitPreReq() - { - return new WinGetConfigResource() - { - Resource = DscHelpers.WinGetDscResource, - Id = DscHelpers.GitWinGetPackageId, - Directives = new() { AllowPrerelease = true, Description = $"Installing {DscHelpers.GitName}" }, - Settings = new WinGetDscSettings() { Id = DscHelpers.GitWinGetPackageId, Source = DscHelpers.DscSourceNameForWinGet }, - }; - } -} + var id = $"{RepoNamePrefix}{task.RepositoryName}{RepoNameSuffix}{task.CloneLocation.FullName}"; + + var gitDependsOnId = DscHelpers.GitWinGetPackageId; + + if (configurationFileKind == ConfigurationFileKind.SetupTarget) + { + gitDependsOnId = $"{DscHelpers.GitWinGetPackageId}{PackageNameSeparator}{DscHelpers.GitName}"; + } + + return new WinGetConfigResource() + { + Resource = DscHelpers.GitCloneDscResource, + Id = id, + Directives = new() { AllowPrerelease = true, Description = $"Cloning: {task.RepositoryName}" }, + DependsOn = [gitDependsOnId], + Settings = new GitDscSettings() { HttpsUrl = webAddress.AbsoluteUri, RootDirectory = task.CloneLocation.FullName }, + }; + } + + /// + /// Creates a WinGetConfigResource object for the GitDsc resource that installs Git for Windows. This is a pre-requisite for + /// the GitDsc resource that clones the repository. + /// + /// The WinGetConfigResource object that represents the block of yaml needed by WinGetDsc to install the Git app. + private WinGetConfigResource CreateWinGetInstallForGitPreReq(ConfigurationFileKind configurationFileKind) + { + var id = DscHelpers.GitWinGetPackageId; + + if (configurationFileKind == ConfigurationFileKind.SetupTarget) + { + // WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI. + // So we add a description to the Id to make it more readable in the UI. These do not need to be localized. + id = $"{DscHelpers.GitWinGetPackageId}{PackageNameSeparator}{DscHelpers.GitName}"; + } + + return new WinGetConfigResource() + { + Resource = DscHelpers.WinGetDscResource, + Id = id, + Directives = new() { AllowPrerelease = true, Description = $"Installing {DscHelpers.GitName}" }, + Settings = new WinGetDscSettings() { Id = DscHelpers.GitWinGetPackageId, Source = DscHelpers.DscSourceNameForWinGet }, + }; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/PackageProvider.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/PackageProvider.cs index e4d6f44eb5..9a728619d9 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/PackageProvider.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/PackageProvider.cs @@ -1,96 +1,96 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using DevHome.SetupFlow.Models; using DevHome.SetupFlow.ViewModels; using Serilog; - + namespace DevHome.SetupFlow.Services; - -/// -/// Class for providing and caching a subset of package view models in order to -/// maintain a consistent package state on the UI. This service should be -/// accessed by the UI thread. -/// -/// -/// For example, if a package was selected from a search result and then the -/// same package appeared in another search query, that package should remain -/// selected, even if the underlying WinGet COM object in memory is different. -/// Furthermore, if the same package appears in popular apps section, the -/// selection should also be reflected in that section and everywhere else on -/// the UI. The same behavior is expected when unselecting the package. -/// -public class PackageProvider + +/// +/// Class for providing and caching a subset of package view models in order to +/// maintain a consistent package state on the UI. This service should be +/// accessed by the UI thread. +/// +/// +/// For example, if a package was selected from a search result and then the +/// same package appeared in another search query, that package should remain +/// selected, even if the underlying WinGet COM object in memory is different. +/// Furthermore, if the same package appears in popular apps section, the +/// selection should also be reflected in that section and everywhere else on +/// the UI. The same behavior is expected when unselecting the package. +/// +public class PackageProvider { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(PackageProvider)); - - private sealed class PackageCache - { - /// - /// Gets or sets the cached package view model - /// - public PackageViewModel PackageViewModel { get; set; } - - /// - /// Gets or sets a value indicating whether the package - /// should be cached temporarily or permanently. - /// - public bool IsPermanent { get; set; } - } - - private readonly PackageViewModelFactory _packageViewModelFactory; + + private sealed class PackageCache + { + /// + /// Gets or sets the cached package view model + /// + public PackageViewModel PackageViewModel { get; set; } + + /// + /// Gets or sets a value indicating whether the package + /// should be cached temporarily or permanently. + /// + public bool IsPermanent { get; set; } + } + + private readonly PackageViewModelFactory _packageViewModelFactory; private readonly object _lock = new(); - - /// - /// Dictionary for caching package view models - /// - /// - /// - Permanently cache packages from catalogs (e.g. Popular apps, restore apps) - /// * Example: A package in popular apps section that also appears in - /// restore apps section and in a search query should have the same - /// visual state when selected or unselected. - /// - Temporarily cache packages that are selected from the search result, - /// and remove them once unselected. - /// * Example: A package that appears in the two different search queries - /// should have the same visual state when selected or unselected. - /// - private readonly Dictionary _packageViewModelCache = new(); - - /// - /// Observable collection containing the list of selected packages in the - /// order they were added - /// - private readonly ObservableCollection _selectedPackages = new(); - - /// - /// Gets a read-only wrapper around the selected package observable collection - /// - public ReadOnlyObservableCollection SelectedPackages => new(_selectedPackages); - - /// - /// Occurs when a package selection has changed - /// - public event EventHandler SelectedPackagesItemChanged; - - public PackageProvider(PackageViewModelFactory packageViewModelFactory) - { - _packageViewModelFactory = packageViewModelFactory; - } - - /// - /// Create a package view model if one does not exist already in the cache, - /// otherwise return the one form the cache - /// - /// WinGet package model - /// - /// True, if the package should be cached permanently. - /// False, if the package should not be cached. - /// - /// Package view model - public PackageViewModel CreateOrGet(IWinGetPackage package, bool cachePermanently = false) + + /// + /// Dictionary for caching package view models + /// + /// + /// - Permanently cache packages from catalogs (e.g. Popular apps, restore apps) + /// * Example: A package in popular apps section that also appears in + /// restore apps section and in a search query should have the same + /// visual state when selected or unselected. + /// - Temporarily cache packages that are selected from the search result, + /// and remove them once unselected. + /// * Example: A package that appears in the two different search queries + /// should have the same visual state when selected or unselected. + /// + private readonly Dictionary _packageViewModelCache = new(); + + /// + /// Observable collection containing the list of selected packages in the + /// order they were added + /// + private readonly ObservableCollection _selectedPackages = new(); + + /// + /// Gets a read-only wrapper around the selected package observable collection + /// + public ReadOnlyObservableCollection SelectedPackages => new(_selectedPackages); + + /// + /// Occurs when a package selection has changed + /// + public event EventHandler SelectedPackagesItemChanged; + + public PackageProvider(PackageViewModelFactory packageViewModelFactory) + { + _packageViewModelFactory = packageViewModelFactory; + } + + /// + /// Create a package view model if one does not exist already in the cache, + /// otherwise return the one form the cache + /// + /// WinGet package model + /// + /// True, if the package should be cached permanently. + /// False, if the package should not be cached. + /// + /// Package view model + public PackageViewModel CreateOrGet(IWinGetPackage package, bool cachePermanently = false) { lock (_lock) { @@ -121,7 +121,7 @@ public PackageViewModel CreateOrGet(IWinGetPackage package, bool cachePermanentl } return viewModel; - } + } } private void OnSelectedPackageVersionChanged(object sender, string version) @@ -132,9 +132,9 @@ private void OnSelectedPackageVersionChanged(object sender, string version) // Notify subscribers that an item in the list of selected packages has changed SelectedPackagesItemChanged?.Invoke(packageViewModel, EventArgs.Empty); } - } - - private void OnPackageSelectionChanged(object sender, bool isSelected) + } + + private void OnPackageSelectionChanged(object sender, bool isSelected) { if (sender is not PackageViewModel packageViewModel) { @@ -180,13 +180,13 @@ private void OnPackageSelectionChanged(object sender, bool isSelected) // Notify subscribers that an item in the list of selected packages has changed SelectedPackagesItemChanged?.Invoke(packageViewModel, EventArgs.Empty); - } - - /// - /// Clear cache and selected packages - /// - public void Clear() - { + } + + /// + /// Clear cache and selected packages + /// + public void Clear() + { lock (_lock) { // Clear cache @@ -197,5 +197,5 @@ public void Clear() _log.Information($"Clearing selected packages"); _selectedPackages.Clear(); } - } -} + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs index 9535a5eaaa..98c5bb9bb8 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs @@ -85,7 +85,13 @@ public static class StringResourceKey public static readonly string SelectedPackagesCount = nameof(SelectedPackagesCount); public static readonly string SetUpButton = nameof(SetUpButton); public static readonly string SizeWithColon = nameof(SizeWithColon); + public static readonly string LoadingPageHeaderLocalText = nameof(LoadingPageHeaderLocalText); + public static readonly string LoadingPageHeaderTargetText = nameof(LoadingPageHeaderTargetText); + public static readonly string LoadingPageSetupTargetText = nameof(LoadingPageSetupTargetText); + public static readonly string LoadingTasksTitleText = nameof(LoadingTasksTitleText); + public static readonly string LoadingLogsTitleText = nameof(LoadingLogsTitleText); public static readonly string LoadingExecutingProgress = nameof(LoadingExecutingProgress); + public static readonly string LoadingExecutingProgressForTarget = nameof(LoadingExecutingProgressForTarget); public static readonly string ActionCenterDisplay = nameof(ActionCenterDisplay); public static readonly string NeedsRebootMessage = nameof(NeedsRebootMessage); public static readonly string ApplicationsPageTitle = nameof(ApplicationsPageTitle); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw index 6bee36d1ba..c2fdb89e8a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -234,19 +234,19 @@ Message displayed when applying a configuration unit succeeds. - {0} : {3} [{1} {2}] + {0}: {3} [{1} {2}] {Locked="{0}","{1}","{2}","{3}"}Summary text for a configuration unit. {0} is replaced by the configuration unit intent (e.g. Assert, Inform, Apply). {1} is replaced by the configuration unit name. {2} is replaced by the configuration unit id. {3} is replaced by the configuration unit description. A string with all values replaced could look like "Apply : Install VS [WinGetPackage vspackage]" - {0} : {1} + {0}: {1} {Locked="{0}","{1}"}Summary text for a configuration unit. {0} is replaced by the configuration unit intent (e.g. Assert, Inform, Apply). {1} is replaced by the configuration unit name. A string with all values replaced could look like "Apply : WinGetPackage" - {0} : {1} [{2}] + {0}: {1} [{2}] {Locked="{0}","{1}","{2}"}Summary text for a configuration unit. {0} is replaced by the configuration unit intent (e.g. Assert, Inform, Apply). {1} is replaced by the configuration unit name. {2} is replaced by the configuration unit id. A string with all values replaced could look like "Apply : WinGetPackage [vspackage]" - {0} : {2} [{1}] + {0}: {2} [{1}] {Locked="{0}","{1}","{2}"}Summary text for a configuration unit. {0} is replaced by the configuration unit intent (e.g. Assert, Inform, Apply). {1} is replaced by the configuration unit name. {2} is replaced by the configuration unit description. A string with all values replaced could look like "Apply : Install VS [WinGetPackage]" @@ -845,9 +845,17 @@ Save Content for the primary button in the edit clone path dialog. + + Set up target: {0} + Shows the name of target device (VM) that is being configured. {0} is the VM name. + Executing step {0}/{1} - Shows ot the user how many tasks have been completed + Shows to the user how many tasks have been completed + + + Configuring the package... + Text shown under progress bar while configuring a target device. Stop @@ -861,13 +869,17 @@ Action center Header for action center - + Tasks Header for executing tasks + + Logs + Header for logs when executing tasks + Failed to clone repository {0} - Main tex tto show in the loading screen that a repo failed to clone + Main text to show in the loading screen that a repo failed to clone Error message: {0} @@ -1024,10 +1036,14 @@ Retry Hyper link button content to allow users to retry all failed tasks - + Setup initiated, stay tuned for the magic... Header for the loading screen + + Hold on while we set things up... + Header for the loading screen + Tasks Header for the Tasks scroll viewer @@ -1080,10 +1096,6 @@ Restart your computer to finish setup Second part of the message notifying users to restart. - - Summary - Title for a section in the summary page - View all Hyperlink button text for revealing the remainder of a trimmed message @@ -1489,7 +1501,7 @@ Text for the completed status of the configuration operation that happened on a remote machine. This is for the entire configuration file we're applying - This part of the configuration is now complete + Configuration applied Text for the completed status of the configuration operation that happened on a remote machine. This is for a single part of the configuration we're applying @@ -1497,7 +1509,7 @@ Text for the 'In progress' status of the configuration operation that happened on a remote machine. This is for the entire configuration file we're applying - This part of the configuration is in progress + Configuration in progress Text for the 'In progress' status of the configuration operation that happened on a remote machine. This is for a single part of the configuration we're applying @@ -1521,7 +1533,7 @@ Text for the 'shut down' status of the configuration operation that happened on a remote machine - This part of the configuration has been skipped + Configuration skipped Text for the 'shut down' status of the configuration operation that happened on a remote machine diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ConfigurationUnitResultViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ConfigurationUnitResultViewModel.cs index 2765d13706..55883676a3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ConfigurationUnitResultViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ConfigurationUnitResultViewModel.cs @@ -1,80 +1,126 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using DevHome.SetupFlow.Common.Exceptions; -using DevHome.SetupFlow.Models; -using DevHome.SetupFlow.Services; -using Microsoft.Management.Configuration; -using Windows.Win32.Foundation; - -namespace DevHome.SetupFlow.ViewModels; - -/// -/// Delegate factory for creating configuration unit result view models -/// -/// Configuration unit result model -/// Configuration unit result view model -public delegate ConfigurationUnitResultViewModel ConfigurationUnitResultViewModelFactory(ConfigurationUnitResult unitResult); - -/// -/// View model for a configuration unit result -/// -public class ConfigurationUnitResultViewModel -{ - private readonly ConfigurationUnitResult _unitResult; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.SetupFlow.Common.Exceptions; +using DevHome.SetupFlow.Models; +using DevHome.SetupFlow.Services; +using Microsoft.Management.Configuration; +using Microsoft.UI.Xaml.Media.Imaging; +using Windows.Win32.Foundation; + +namespace DevHome.SetupFlow.ViewModels; + +/// +/// Delegate factory for creating configuration unit result view models +/// +/// Configuration unit result model +/// Configuration unit result view model +public delegate ConfigurationUnitResultViewModel ConfigurationUnitResultViewModelFactory(ConfigurationUnitResult unitResult); + +/// +/// View model for a configuration unit result +/// +public class ConfigurationUnitResultViewModel +{ + private readonly ConfigurationUnitResult _unitResult; private readonly ISetupFlowStringResource _stringResource; - - public ConfigurationUnitResultViewModel(ISetupFlowStringResource stringResource, ConfigurationUnitResult unitResult) - { - _stringResource = stringResource; - _unitResult = unitResult; - } - - public string Title => BuildTitle(); - - public string ApplyResult => GetApplyResult(); + + public ConfigurationUnitResultViewModel(ISetupFlowStringResource stringResource, ConfigurationUnitResult unitResult) + { + _stringResource = stringResource; + _unitResult = unitResult; + SetName(); + } + + public string Title => BuildTitle(); + + public string ApplyResult => GetApplyResult(); public string ErrorDescription => GetErrorDescription(); - - public bool IsSkipped => _unitResult.IsSkipped; - - public bool IsError => !IsSkipped && _unitResult.HResult != HRESULT.S_OK; - - public bool IsSuccess => _unitResult.HResult == HRESULT.S_OK; - - private string GetApplyResult() - { - if (IsSkipped) - { - return GetUnitSkipMessage(); + + public bool IsSkipped => _unitResult.IsSkipped; + + public bool IsError => !IsSkipped && _unitResult.HResult != HRESULT.S_OK; + + public bool IsSuccess => _unitResult.HResult == HRESULT.S_OK; + + public bool IsCloneRepoUnit => string.Equals(_unitResult.Type, "GitClone", System.StringComparison.OrdinalIgnoreCase); + + public bool IsPrivateRepo => false; + + public string Name { get; private set; } + + public BitmapImage StatusSymbolIcon { get; set; } + + private void SetName() + { + // Get either Git repo name for if this is a repo clone request or package name for package install request. + // We add into Id in ConfigurationFileBuilder. + if (!string.IsNullOrEmpty(_unitResult.Id)) + { + if (IsCloneRepoUnit) + { + var start = ConfigurationFileBuilder.RepoNamePrefix.Length; + var descriptionParts = _unitResult.Id.Substring(start).Split(ConfigurationFileBuilder.RepoNameSuffix); + Name = descriptionParts[0]; + } + else + { + var descriptionParts = _unitResult.Id.Split(ConfigurationFileBuilder.PackageNameSeparator); + + if (descriptionParts.Length > 1) + { + Name = descriptionParts[1]; + } + else + { + Name = descriptionParts[0]; + } + } + } + else if (!string.IsNullOrEmpty(_unitResult.UnitDescription)) + { + Name = _unitResult.UnitDescription; + } + else + { + Name = ""; + } + } + + private string GetApplyResult() + { + if (IsSkipped) + { + return GetUnitSkipMessage(); } if (IsError) { return GetUnitErrorMessage(); - } - - return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSuccess); + } + + return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSuccess); } - - private string BuildTitle() - { - if (string.IsNullOrEmpty(_unitResult.Id) && string.IsNullOrEmpty(_unitResult.UnitDescription)) - { - return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryMinimal, _unitResult.Intent, _unitResult.Type); - } - - if (string.IsNullOrEmpty(_unitResult.Id)) - { - return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryNoId, _unitResult.Intent, _unitResult.Type, _unitResult.UnitDescription); - } - - if (string.IsNullOrEmpty(_unitResult.UnitDescription)) - { - return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryNoDescription, _unitResult.Intent, _unitResult.Type, _unitResult.Id); - } - - return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryFull, _unitResult.Intent, _unitResult.Type, _unitResult.Id, _unitResult.UnitDescription); + + private string BuildTitle() + { + if (string.IsNullOrEmpty(_unitResult.Id) && string.IsNullOrEmpty(_unitResult.UnitDescription)) + { + return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryMinimal, _unitResult.Intent, _unitResult.Type); + } + + if (string.IsNullOrEmpty(_unitResult.Id)) + { + return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryNoId, _unitResult.Intent, _unitResult.Type, _unitResult.UnitDescription); + } + + if (string.IsNullOrEmpty(_unitResult.UnitDescription)) + { + return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryNoDescription, _unitResult.Intent, _unitResult.Type, _unitResult.Id); + } + + return _stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryFull, _unitResult.Intent, _unitResult.Type, _unitResult.Id, _unitResult.UnitDescription); } private string GetUnitSkipMessage() @@ -179,4 +225,4 @@ private string GetErrorDescription() return _unitResult.ErrorDescription; } } -} +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingViewModel.cs index a4c38c1878..deeafbeb1c 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingViewModel.cs @@ -38,9 +38,11 @@ public partial class LoadingViewModel : SetupPageViewModelBase private static readonly BitmapImage DarkCaution = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/DarkCaution.png")); private static readonly BitmapImage DarkError = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/DarkError.png")); private static readonly BitmapImage DarkSuccess = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/DarkSuccess.png")); + private static readonly BitmapImage DarkInfo = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/DarkCaution.png")); private static readonly BitmapImage LightCaution = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/LightCaution.png")); private static readonly BitmapImage LightError = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/LightError.png")); private static readonly BitmapImage LightSuccess = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/LightSuccess.png")); + private static readonly BitmapImage LightInfo = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/LightCaution.png")); #pragma warning disable SA1310 // Field names should not contain underscore private const int MAX_RETRIES = 1; @@ -111,6 +113,36 @@ public partial class LoadingViewModel : SetupPageViewModelBase [ObservableProperty] private int _tasksFailed; + /// + /// Used in the UI header text. + /// + [ObservableProperty] + private string _headerText; + + /// + /// Used in the UI as title for the executed tasks/logs. + /// + [ObservableProperty] + private string _tasksTitleText; + + /// + /// Used in the UI as setup target text (target name). + /// + [ObservableProperty] + private string _setupTargetText; + + /// + /// Controls if SetupTargetText should be shown. + /// + [ObservableProperty] + private bool _showSetupTarget; + + /// + /// Controls if indeterminate progress bar should be shown. + /// + [ObservableProperty] + private bool _isIndeterminateProgressBar; + /// /// Used in the UI to show the user how many tasks have been executed. /// @@ -165,6 +197,11 @@ public async Task RestartFailedTasksAsync() _failedTasks.Clear(); ActionCenterItems = new ObservableCollection(); ShowRetryButton = Visibility.Collapsed; + if (Orchestrator.IsSettingUpATargetMachine) + { + IsIndeterminateProgressBar = true; + } + await StartAllTasks(TasksToRun); } @@ -201,6 +238,11 @@ public void AddMessage(string message, MessageSeverityKind severityKind = Messag messageToDisplay.ShouldShowStatusSymbolIcon = true; messageToDisplay.StatusSymbolIcon = (_currentTheme == ElementTheme.Dark) ? DarkSuccess : LightSuccess; } + else if (severityKind == MessageSeverityKind.Info) + { + messageToDisplay.ShouldShowStatusSymbolIcon = true; + messageToDisplay.StatusSymbolIcon = (_currentTheme == ElementTheme.Dark) ? DarkInfo : LightInfo; + } Messages.Insert(Messages.Count - _numberOfExecutingTasks, messageToDisplay); }); @@ -217,9 +259,9 @@ public void UpdateActionCenterMessage(ActionCenterMessages message, ActionMessag // the user switches between different Dev Home pages and then comes back to the loading screen when an adaptive card is // loaded into the action center. var items = ActionCenterItems.ToList(); - if (requestKind == ActionMessageRequestKind.Add) + if (requestKind == ActionMessageRequestKind.Add) { - items.Add(message); + items.Add(message); } else { @@ -245,6 +287,22 @@ public LoadingViewModel( IsStepPage = false; IsNavigationBarVisible = false; NextPageButtonText = stringResource.GetLocalized(StringResourceKey.LoadingScreenGoToSummaryButtonContent); + if (Orchestrator.IsSettingUpATargetMachine) + { + HeaderText = StringResource.GetLocalized(StringResourceKey.LoadingPageHeaderTargetText); + TasksTitleText = StringResource.GetLocalized(StringResourceKey.LoadingLogsTitleText); + ExecutingTasks = StringResource.GetLocalized(StringResourceKey.LoadingExecutingProgressForTarget); + ShowSetupTarget = true; + IsIndeterminateProgressBar = true; + } + else + { + HeaderText = StringResource.GetLocalized(StringResourceKey.LoadingPageHeaderLocalText); + TasksTitleText = StringResource.GetLocalized(StringResourceKey.LoadingTasksTitleText); + SetupTargetText = string.Empty; + IsIndeterminateProgressBar = false; + } + ShowRetryButton = Visibility.Collapsed; _failedTasks = new List(); ActionCenterItems = new(); @@ -302,7 +360,11 @@ private void FetchTaskInformation() /// private void SetExecutingTaskAndActionCenter() { - ExecutingTasks = StringResource.GetLocalized(StringResourceKey.LoadingExecutingProgress, TasksStarted, TasksToRun.Count); + if (!Orchestrator.IsSettingUpATargetMachine) + { + ExecutingTasks = StringResource.GetLocalized(StringResourceKey.LoadingExecutingProgress, TasksStarted, TasksToRun.Count); + } + ActionCenterDisplay = StringResource.GetLocalized(StringResourceKey.ActionCenterDisplay, 0); } @@ -428,6 +490,8 @@ await Parallel.ForEachAsync(tasksToRunSecond, async (taskInformation, token) => }); // All the tasks are done. Re-try logic follows. + IsIndeterminateProgressBar = false; + if (_failedTasks.Count == 0) { _log.Information("All tasks succeeded. Moving to next page"); @@ -471,7 +535,14 @@ private async Task StartTaskAndReportResult(WinUIEx.WindowEx window, TaskInforma window.DispatcherQueue.TryEnqueue(() => { TasksStarted++; - ExecutingTasks = StringResource.GetLocalized(StringResourceKey.LoadingExecutingProgress, TasksStarted, TasksToRun.Count); + if (!Orchestrator.IsSettingUpATargetMachine) + { + ExecutingTasks = StringResource.GetLocalized(StringResourceKey.LoadingExecutingProgress, TasksStarted, TasksToRun.Count); + } + else + { + SetupTargetText = StringResource.GetLocalized(StringResourceKey.LoadingPageSetupTargetText, taskInformation.TaskToExecute.TargetName); + } loadingMessage.ShouldShowProgressRing = true; Messages.Add(loadingMessage); @@ -493,7 +564,7 @@ private async Task StartTaskAndReportResult(WinUIEx.WindowEx window, TaskInforma window.DispatcherQueue.TryEnqueue(() => { - // Keep decrement inside TryEnqueue to encorce "locking" + // Keep decrement inside TryEnqueue to enforce "locking" _numberOfExecutingTasks--; ChangeMessage(taskInformation, loadingMessage, taskFinishedState); TasksCompleted++; diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageViewModel.cs index 6008195d6b..135c18d727 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageViewModel.cs @@ -1,66 +1,66 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using DevHome.Common.Services; -using DevHome.Contracts.Services; -using DevHome.SetupFlow.Models; -using DevHome.SetupFlow.Services; -using Microsoft.Internal.Windows.DevHome.Helpers.Restore; -using Microsoft.UI.Xaml.Media.Imaging; -using Windows.Storage.Streams; -using Windows.System; - -namespace DevHome.SetupFlow.ViewModels; - -/// -/// Delegate factory for creating package view models -/// -/// WinGet package -/// Package view model -public delegate PackageViewModel PackageViewModelFactory(IWinGetPackage package); - -/// -/// ViewModel class for the model. -/// -public partial class PackageViewModel : ObservableObject -{ - private const string PublisherNameNotAvailable = "-"; - - private static readonly BitmapImage DefaultLightPackageIconSource = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/DefaultLightPackageIcon.png")); - private static readonly BitmapImage DefaultDarkPackageIconSource = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/DefaultDarkPackageIcon.png")); - - private readonly Lazy _packageDarkThemeIcon; - private readonly Lazy _packageLightThemeIcon; - - private readonly ISetupFlowStringResource _stringResource; - private readonly IWinGetPackage _package; - private readonly IWindowsPackageManager _wpm; - private readonly IThemeSelectorService _themeSelector; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Services; +using DevHome.Contracts.Services; +using DevHome.SetupFlow.Models; +using DevHome.SetupFlow.Services; +using Microsoft.Internal.Windows.DevHome.Helpers.Restore; +using Microsoft.UI.Xaml.Media.Imaging; +using Windows.Storage.Streams; +using Windows.System; + +namespace DevHome.SetupFlow.ViewModels; + +/// +/// Delegate factory for creating package view models +/// +/// WinGet package +/// Package view model +public delegate PackageViewModel PackageViewModelFactory(IWinGetPackage package); + +/// +/// ViewModel class for the model. +/// +public partial class PackageViewModel : ObservableObject +{ + private const string PublisherNameNotAvailable = "-"; + + private static readonly BitmapImage DefaultLightPackageIconSource = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/DefaultLightPackageIcon.png")); + private static readonly BitmapImage DefaultDarkPackageIconSource = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/DefaultDarkPackageIcon.png")); + + private readonly Lazy _packageDarkThemeIcon; + private readonly Lazy _packageLightThemeIcon; + + private readonly ISetupFlowStringResource _stringResource; + private readonly IWinGetPackage _package; + private readonly IWindowsPackageManager _wpm; + private readonly IThemeSelectorService _themeSelector; private readonly IScreenReaderService _screenReaderService; - private readonly SetupFlowOrchestrator _orchestrator; - - /// - /// Occurs after the package selection changes - /// - public event EventHandler SelectionChanged; + private readonly SetupFlowOrchestrator _orchestrator; + + /// + /// Occurs after the package selection changes + /// + public event EventHandler SelectionChanged; /// /// Occurs after the package version has changed /// public event EventHandler VersionChanged; - - /// - /// Indicates if a package is selected - /// - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ButtonAutomationName))] - private bool _isSelected; - + + /// + /// Indicates if a package is selected + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ButtonAutomationName))] + private bool _isSelected; + [ObservableProperty] [NotifyPropertyChangedFor(nameof(TooltipVersion))] [NotifyPropertyChangedFor(nameof(PackageFullDescription))] @@ -68,108 +68,108 @@ public partial class PackageViewModel : ObservableObject public bool ShowVersionList => IsVersioningSupported(); - public PackageViewModel( - ISetupFlowStringResource stringResource, - IWindowsPackageManager wpm, - IWinGetPackage package, - IThemeSelectorService themeSelector, - IScreenReaderService screenReaderService, - SetupFlowOrchestrator orchestrator) - { - _stringResource = stringResource; - _wpm = wpm; - _package = package; - _themeSelector = themeSelector; + public PackageViewModel( + ISetupFlowStringResource stringResource, + IWindowsPackageManager wpm, + IWinGetPackage package, + IThemeSelectorService themeSelector, + IScreenReaderService screenReaderService, + SetupFlowOrchestrator orchestrator) + { + _stringResource = stringResource; + _wpm = wpm; + _package = package; + _themeSelector = themeSelector; _screenReaderService = screenReaderService; - _orchestrator = orchestrator; - - // Lazy-initialize optional or expensive view model members - _packageDarkThemeIcon = new Lazy(() => GetIconByTheme(RestoreApplicationIconTheme.Dark)); - _packageLightThemeIcon = new Lazy(() => GetIconByTheme(RestoreApplicationIconTheme.Light)); + _orchestrator = orchestrator; + + // Lazy-initialize optional or expensive view model members + _packageDarkThemeIcon = new Lazy(() => GetIconByTheme(RestoreApplicationIconTheme.Dark)); + _packageLightThemeIcon = new Lazy(() => GetIconByTheme(RestoreApplicationIconTheme.Light)); SelectedVersion = GetDefaultSelectedVersion(); InstallPackageTask = CreateInstallTask(); - } - - public PackageUniqueKey UniqueKey => _package.UniqueKey; - - public IWinGetPackage Package => _package; - - public BitmapImage Icon => _themeSelector.IsDarkTheme() ? _packageDarkThemeIcon.Value : _packageLightThemeIcon.Value; - - public string Name => _package.Name; - - public string InstalledVersion => _package.InstalledVersion; - + } + + public PackageUniqueKey UniqueKey => _package.UniqueKey; + + public IWinGetPackage Package => _package; + + public BitmapImage Icon => _themeSelector.IsDarkTheme() ? _packageDarkThemeIcon.Value : _packageLightThemeIcon.Value; + + public string Name => _package.Name; + + public string InstalledVersion => _package.InstalledVersion; + public IReadOnlyList AvailableVersions => _package.AvailableVersions; - - public bool IsInstalled => _package.IsInstalled; - - public string CatalogName => _package.CatalogName; - + + public bool IsInstalled => _package.IsInstalled; + + public string CatalogName => _package.CatalogName; + public string PublisherName => string.IsNullOrWhiteSpace(_package.PublisherName) ? PublisherNameNotAvailable : _package.PublisherName; - public string InstallationNotes => _package.InstallationNotes; + public string InstallationNotes => _package.InstallationNotes; public string PackageFullDescription => GetPackageFullDescription(); - + public string PackageShortDescription => GetPackageShortDescription(); - - public string PackageTitle => Name; - - public string TooltipName => _stringResource.GetLocalized(StringResourceKey.PackageNameTooltip, Name); - - public string TooltipVersion => _stringResource.GetLocalized(StringResourceKey.PackageVersionTooltip, SelectedVersion); - - public string TooltipIsInstalled => IsInstalled ? _stringResource.GetLocalized(StringResourceKey.PackageInstalledTooltip) : string.Empty; - - public string TooltipSource => _stringResource.GetLocalized(StringResourceKey.PackageSourceTooltip, CatalogName); - - public string TooltipPublisher => _stringResource.GetLocalized(StringResourceKey.PackagePublisherNameTooltip, PublisherName); + + public string PackageTitle => Name; + + public string TooltipName => _stringResource.GetLocalized(StringResourceKey.PackageNameTooltip, Name); + + public string TooltipVersion => _stringResource.GetLocalized(StringResourceKey.PackageVersionTooltip, SelectedVersion); + + public string TooltipIsInstalled => IsInstalled ? _stringResource.GetLocalized(StringResourceKey.PackageInstalledTooltip) : string.Empty; + + public string TooltipSource => _stringResource.GetLocalized(StringResourceKey.PackageSourceTooltip, CatalogName); + + public string TooltipPublisher => _stringResource.GetLocalized(StringResourceKey.PackagePublisherNameTooltip, PublisherName); public bool CanInstall => _orchestrator.IsSettingUpATargetMachine || !IsInstalled || _package.InstalledVersion != SelectedVersion; - - public string ButtonAutomationName => IsSelected ? - _stringResource.GetLocalized(StringResourceKey.RemoveApplication) : - _stringResource.GetLocalized(StringResourceKey.AddApplication); - - public InstallPackageTask InstallPackageTask { get; private set; } - - /// - /// Gets the URI for the "Learn more" button - /// - /// - /// For packages from winget or custom catalogs: - /// 1. Use package url - /// 2. Else, use publisher url - /// 3. Else, use "https://github.com/microsoft/winget-pkgs" - /// - /// For packages from ms store catalog: - /// 1. Use package url - /// 2. Else, use "ms-windows-store://pdp?productid={ID}" - /// - /// Learn more button uri - public Uri GetLearnMoreUri() - { - if (_package.PackageUrl != null) - { - return _package.PackageUrl; - } - - if (_wpm.IsMsStorePackage(_package)) - { - return new Uri($"ms-windows-store://pdp/?productid={_package.Id}"); - } - - if (_package.PublisherUrl != null) - { - return _package.PublisherUrl; - } - - return new Uri("https://github.com/microsoft/winget-pkgs"); - } - - partial void OnIsSelectedChanged(bool value) => SelectionChanged?.Invoke(this, value); + + public string ButtonAutomationName => IsSelected ? + _stringResource.GetLocalized(StringResourceKey.RemoveApplication) : + _stringResource.GetLocalized(StringResourceKey.AddApplication); + + public InstallPackageTask InstallPackageTask { get; private set; } + + /// + /// Gets the URI for the "Learn more" button + /// + /// + /// For packages from winget or custom catalogs: + /// 1. Use package url + /// 2. Else, use publisher url + /// 3. Else, use "https://github.com/microsoft/winget-pkgs" + /// + /// For packages from ms store catalog: + /// 1. Use package url + /// 2. Else, use "ms-windows-store://pdp?productid={ID}" + /// + /// Learn more button uri + public Uri GetLearnMoreUri() + { + if (_package.PackageUrl != null) + { + return _package.PackageUrl; + } + + if (_wpm.IsMsStorePackage(_package)) + { + return new Uri($"ms-windows-store://pdp/?productid={_package.Id}"); + } + + if (_package.PublisherUrl != null) + { + return _package.PublisherUrl; + } + + return new Uri("https://github.com/microsoft/winget-pkgs"); + } + + partial void OnIsSelectedChanged(bool value) => SelectionChanged?.Invoke(this, value); partial void OnSelectedVersionChanged(string value) { @@ -179,53 +179,53 @@ partial void OnSelectedVersionChanged(string value) VersionChanged?.Invoke(this, SelectedVersion); } - /// - /// Toggle package selection - /// - [RelayCommand] - private void ToggleSelection() - { - // TODO Explore option to augment a Button with the option to announce a text when invoked. - // https://github.com/microsoft/devhome/issues/1451 - var announcementText = IsSelected ? - _stringResource.GetLocalized(StringResourceKey.RemovedApplication, PackageTitle) : - _stringResource.GetLocalized(StringResourceKey.AddedApplication, PackageTitle); - - IsSelected = !IsSelected; - _screenReaderService.Announce(announcementText); - } - - /// - /// Gets the package icon based on the provided theme - /// - /// Package icon theme - /// Package icon - private BitmapImage GetIconByTheme(RestoreApplicationIconTheme theme) - { - return theme switch - { - // Get default dark theme icon if corresponding package icon was not found - RestoreApplicationIconTheme.Dark => - _package.DarkThemeIcon == null ? DefaultDarkPackageIconSource : CreateBitmapImage(_package.DarkThemeIcon), - - // Get default light theme icon if corresponding package icon was not found - _ => _package.LightThemeIcon == null ? DefaultLightPackageIconSource : CreateBitmapImage(_package.LightThemeIcon), - }; - } - - private BitmapImage CreateBitmapImage(IRandomAccessStream stream) - { - var bitmapImage = new BitmapImage(); - stream.Seek(0); - bitmapImage.SetSource(stream); - return bitmapImage; - } - - private InstallPackageTask CreateInstallTask() - { - return _package.CreateInstallTask(_wpm, _stringResource, SelectedVersion, _orchestrator.ActivityId); - } - + /// + /// Toggle package selection + /// + [RelayCommand] + private void ToggleSelection() + { + // TODO Explore option to augment a Button with the option to announce a text when invoked. + // https://github.com/microsoft/devhome/issues/1451 + var announcementText = IsSelected ? + _stringResource.GetLocalized(StringResourceKey.RemovedApplication, PackageTitle) : + _stringResource.GetLocalized(StringResourceKey.AddedApplication, PackageTitle); + + IsSelected = !IsSelected; + _screenReaderService.Announce(announcementText); + } + + /// + /// Gets the package icon based on the provided theme + /// + /// Package icon theme + /// Package icon + private BitmapImage GetIconByTheme(RestoreApplicationIconTheme theme) + { + return theme switch + { + // Get default dark theme icon if corresponding package icon was not found + RestoreApplicationIconTheme.Dark => + _package.DarkThemeIcon == null ? DefaultDarkPackageIconSource : CreateBitmapImage(_package.DarkThemeIcon), + + // Get default light theme icon if corresponding package icon was not found + _ => _package.LightThemeIcon == null ? DefaultLightPackageIconSource : CreateBitmapImage(_package.LightThemeIcon), + }; + } + + private BitmapImage CreateBitmapImage(IRandomAccessStream stream) + { + var bitmapImage = new BitmapImage(); + stream.Seek(0); + bitmapImage.SetSource(stream); + return bitmapImage; + } + + private InstallPackageTask CreateInstallTask() + { + return _package.CreateInstallTask(_wpm, _stringResource, SelectedVersion, _orchestrator.ActivityId); + } + private string GetPackageShortDescription() { // Source | Publisher name @@ -236,33 +236,33 @@ private string GetPackageShortDescription() // Source return CatalogName; - } - - private string GetPackageFullDescription() - { - // Version | Source | Publisher name - if (!_wpm.IsMsStorePackage(_package) && !string.IsNullOrEmpty(_package.PublisherName)) - { - return _stringResource.GetLocalized(StringResourceKey.PackageDescriptionThreeParts, SelectedVersion, CatalogName, PublisherName); - } - - // Version | Source - if (!_wpm.IsMsStorePackage(_package)) - { - return _stringResource.GetLocalized(StringResourceKey.PackageDescriptionTwoParts, SelectedVersion, CatalogName); - } - - // Source | Publisher name - if (!string.IsNullOrEmpty(_package.PublisherName)) - { - return _stringResource.GetLocalized(StringResourceKey.PackageDescriptionTwoParts, CatalogName, PublisherName); - } - - // Source - return CatalogName; - } - - /// + } + + private string GetPackageFullDescription() + { + // Version | Source | Publisher name + if (!_wpm.IsMsStorePackage(_package) && !string.IsNullOrEmpty(_package.PublisherName)) + { + return _stringResource.GetLocalized(StringResourceKey.PackageDescriptionThreeParts, SelectedVersion, CatalogName, PublisherName); + } + + // Version | Source + if (!_wpm.IsMsStorePackage(_package)) + { + return _stringResource.GetLocalized(StringResourceKey.PackageDescriptionTwoParts, SelectedVersion, CatalogName); + } + + // Source | Publisher name + if (!string.IsNullOrEmpty(_package.PublisherName)) + { + return _stringResource.GetLocalized(StringResourceKey.PackageDescriptionTwoParts, CatalogName, PublisherName); + } + + // Source + return CatalogName; + } + + /// /// Indicates if a specific version of the package can be selected to install /// /// True a package version can be specified to install @@ -280,13 +280,13 @@ private string GetDefaultSelectedVersion() { return _package.IsInstalled ? _package.InstalledVersion : _package.DefaultInstallVersion; } - - /// - /// Command for launching a 'Learn more' uri - /// - [RelayCommand] - private async Task LearnMoreAsync() - { - await Launcher.LaunchUriAsync(GetLearnMoreUri()); - } -} + + /// + /// Command for launching a 'Learn more' uri + /// + [RelayCommand] + private async Task LearnMoreAsync() + { + await Launcher.LaunchUriAsync(GetLearnMoreUri()); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs index 8926e1b559..0eb07e27c6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs @@ -41,15 +41,15 @@ public partial class ReviewViewModel : SetupPageViewModelBase [NotifyCanExecuteChangedFor(nameof(SetUpCommand))] private bool _canSetUp; - [ObservableProperty] + [ObservableProperty] private string _reviewPageTitle; - [ObservableProperty] + [ObservableProperty] private string _reviewPageExpanderDescription; - [ObservableProperty] - private string _reviewPageDescription; - + [ObservableProperty] + private string _reviewPageDescription; + public bool HasApplicationsToInstall => Orchestrator.GetTaskGroup()?.SetupTasks.Any() == true; public bool RequiresTermsAgreement => HasApplicationsToInstall; @@ -116,15 +116,15 @@ protected async override Task OnEachNavigateToAsync() SelectedReviewTab = ReviewTabs.FirstOrDefault(); // If the CreateEnvironmentTaskGroup is present, update the setup button text to "Create Environment" - // and page title to "Review your environment" - if (Orchestrator.GetTaskGroup() != null) - { - NextPageButtonText = StringResource.GetLocalized(StringResourceKey.CreateEnvironmentButtonText); + // and page title to "Review your environment" + if (Orchestrator.GetTaskGroup() != null) + { + NextPageButtonText = StringResource.GetLocalized(StringResourceKey.CreateEnvironmentButtonText); PageTitle = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewPageTitle); ReviewPageExpanderDescription = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewExpanderDescription); - ReviewPageDescription = StringResource.GetLocalized(StringResourceKey.SetupShellReviewPageDescriptionForEnvironmentCreation); - } - + ReviewPageDescription = StringResource.GetLocalized(StringResourceKey.SetupShellReviewPageDescriptionForEnvironmentCreation); + } + NextPageButtonToolTipText = HasTasksToSetUp ? null : StringResource.GetLocalized(StringResourceKey.ReviewNothingToSetUpToolTip); UpdateCanSetUp(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs index b6e45248dc..33295f4af2 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs @@ -37,7 +37,6 @@ public partial class SummaryViewModel : SetupPageViewModelBase private readonly SetupFlowOrchestrator _orchestrator; private readonly SetupFlowViewModel _setupFlowViewModel; private readonly IHost _host; - private readonly Lazy> _configurationUnitResults; private readonly ConfigurationUnitResultViewModelFactory _configurationUnitResultViewModelFactory; private readonly PackageProvider _packageProvider; private readonly IAppManagementInitializer _appManagementInitializer; @@ -76,21 +75,25 @@ public ObservableCollection RepositoriesCloned get { var repositoriesCloned = new ObservableCollection(); - var taskGroup = _host.GetService().TaskGroups; - var group = taskGroup.SingleOrDefault(x => x.GetType() == typeof(RepoConfigTaskGroup)); - if (group is RepoConfigTaskGroup repoTaskGroup) + if (!IsSettingUpATargetMachine) { - foreach (var task in repoTaskGroup.SetupTasks) + var taskGroup = _host.GetService().TaskGroups; + var group = taskGroup.SingleOrDefault(x => x.GetType() == typeof(RepoConfigTaskGroup)); + if (group is RepoConfigTaskGroup repoTaskGroup) { - if (task is CloneRepoTask repoTask && repoTask.WasCloningSuccessful) + foreach (var task in repoTaskGroup.SetupTasks) { - repositoriesCloned.Add(new(repoTask.RepositoryToClone)); + if (task is CloneRepoTask repoTask && repoTask.WasCloningSuccessful) + { + repositoriesCloned.Add(new(repoTask.RepositoryToClone)); + } } } + + var localizedHeader = (repositoriesCloned.Count == 1) ? StringResourceKey.SummaryPageOneRepositoryCloned : StringResourceKey.SummaryPageReposClonedCount; + RepositoriesClonedText = StringResource.GetLocalized(localizedHeader); } - var localizedHeader = (repositoriesCloned.Count == 1) ? StringResourceKey.SummaryPageOneRepositoryCloned : StringResourceKey.SummaryPageReposClonedCount; - RepositoriesClonedText = StringResource.GetLocalized(localizedHeader); return repositoriesCloned; } } @@ -100,10 +103,14 @@ public ObservableCollection AppsDownloaded get { var packagesInstalled = new ObservableCollection(); - var packages = _packageProvider.SelectedPackages.Where(sp => sp.CanInstall && sp.InstallPackageTask.WasInstallSuccessful).ToList(); - packages.ForEach(p => packagesInstalled.Add(p)); - var localizedHeader = (packagesInstalled.Count == 1) ? StringResourceKey.SummaryPageOneApplicationInstalled : StringResourceKey.SummaryPageAppsDownloadedCount; - ApplicationsClonedText = StringResource.GetLocalized(localizedHeader); + if (!IsSettingUpATargetMachine) + { + var packages = _packageProvider.SelectedPackages.Where(sp => sp.CanInstall && sp.InstallPackageTask.WasInstallSuccessful).ToList(); + packages.ForEach(p => packagesInstalled.Add(p)); + var localizedHeader = (packagesInstalled.Count == 1) ? StringResourceKey.SummaryPageOneApplicationInstalled : StringResourceKey.SummaryPageAppsDownloadedCount; + ApplicationsClonedText = StringResource.GetLocalized(localizedHeader); + } + return packagesInstalled; } } @@ -124,11 +131,23 @@ public bool WasCreateEnvironmentOperationStarted public List AppsDownloadedInstallationNotes => AppsDownloaded.Where(p => !string.IsNullOrEmpty(p.InstallationNotes)).ToList(); - public IList ConfigurationUnitResults => _configurationUnitResults.Value; + public List ConfigurationUnitResults { get; private set; } = []; + + public List TargetCloneResults { get; private set; } = []; + + public List TargetInstallResults { get; private set; } = []; + + public List TargetFailedResults { get; private set; } = []; - public bool ShowConfigurationUnitResults => ConfigurationUnitResults.Any(); + public bool IsSettingUpATargetMachine => _orchestrator.IsSettingUpATargetMachine; - public bool CompletedWithErrors => ConfigurationUnitResults.Any(unitResult => unitResult.IsError); + public bool ShowConfigurationUnitResults => ConfigurationUnitResults.Count > 0; + + public bool ShowTargetMachineSetupResults => IsSettingUpATargetMachine && ShowConfigurationUnitResults; + + public bool ShowConfigurationFileResults => ShowConfigurationUnitResults && !IsSettingUpATargetMachine; + + public bool CompletedWithErrors => TargetFailedResults.Count > 0 || FailedTasks.Count > 0; public int ConfigurationUnitSucceededCount => ConfigurationUnitResults.Count(unitResult => unitResult.IsSuccess); @@ -237,7 +256,7 @@ public SummaryViewModel( _host = host; _configurationUnitResultViewModelFactory = configurationUnitResultViewModelFactory; _packageProvider = packageProvider; - _configurationUnitResults = new(GetConfigurationUnitResults); + _showRestartNeeded = Visibility.Collapsed; _appManagementInitializer = appManagementInitializer; _cloneRepoNextSteps = new(); @@ -250,6 +269,17 @@ public SummaryViewModel( protected async override Task OnFirstNavigateToAsync() { + ConfigurationUnitResults = GetConfigurationUnitResults(); + + TargetCloneResults = InitializeTargetResults( + unitResult => unitResult.IsCloneRepoUnit && unitResult.IsSuccess && !unitResult.IsSkipped); + + TargetInstallResults = InitializeTargetResults( + unitResult => !unitResult.IsCloneRepoUnit && unitResult.IsSuccess && !unitResult.IsSkipped); + + TargetFailedResults = InitializeTargetResults( + unitResult => unitResult.IsError); + IList failedTasks = new List(); // Find the loading view model. @@ -296,6 +326,20 @@ protected async override Task OnFirstNavigateToAsync() FailedTasks.Add(summaryMessageViewModel); } + if (IsSettingUpATargetMachine) + { + foreach (var targetFailedResult in TargetFailedResults) + { + targetFailedResult.StatusSymbolIcon = statusSymbol; + } + + var localizedHeader = (TargetCloneResults.Count == 1) ? StringResourceKey.SummaryPageOneRepositoryCloned : StringResourceKey.SummaryPageReposClonedCount; + RepositoriesClonedText = StringResource.GetLocalized(localizedHeader); + + localizedHeader = (TargetInstallResults.Count == 1) ? StringResourceKey.SummaryPageOneApplicationInstalled : StringResourceKey.SummaryPageAppsDownloadedCount; + ApplicationsClonedText = StringResource.GetLocalized(localizedHeader); + } + // If any tasks failed in the loading screen, the user has to click on the "Next" button // If no tasks failed, the user is brought to the summary screen, no interaction required. if (failedTasks.Count != 0) @@ -341,7 +385,7 @@ private List GetConfigurationUnitResults() List unitResults = new(); // If we are setting up a target machine, we need to get the configuration results from the setup target task group. - if (_orchestrator.IsSettingUpATargetMachine) + if (IsSettingUpATargetMachine) { var setupTaskGroup = _orchestrator.GetTaskGroup(); if (setupTaskGroup?.ConfigureTask?.ConfigurationResults != null) @@ -360,4 +404,15 @@ private List GetConfigurationUnitResults() return unitResults; } + + private List InitializeTargetResults(Func predicate) + { + List unitResults = new(); + if (IsSettingUpATargetMachine) + { + unitResults.AddRange(ConfigurationUnitResults.Where(unitResult => predicate(unitResult))); + } + + return unitResults; + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/LoadingView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/LoadingView.xaml index d1f862c464..494950f850 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/LoadingView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/LoadingView.xaml @@ -95,8 +95,11 @@ - - + + + + + - + @@ -160,14 +163,15 @@ + VerticalAlignment="Top" + Visibility="{x:Bind ShouldShowStatusSymbolIcon, Mode=OneWay}" + Source="{x:Bind StatusSymbolIcon, Mode=OneWay}"/> + IsActive="{x:Bind ShouldShowProgressRing, Mode=OneWay}" + Visibility="{x:Bind ShouldShowProgressRing, Mode=OneWay}"/> diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml index ab86d65342..13d7432fa0 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml @@ -28,6 +28,8 @@ + + @@ -92,7 +94,7 @@ + Text="{x:Bind ViewModel.PageHeaderText}"/> - + + @@ -183,6 +186,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + Grid.Row="1" + Visibility="{x:Bind ViewModel.ShouldShowCollectionView, Mode=OneWay}"> + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Visibility="{x:Bind ViewModel.ShouldShowShimmerBelowList, Mode=OneWay}" + IsTabStop="False" + Margin="0 30 0 0"> + - - - - - - - - - + + + + + + + + + - - + + From eb9b2c89b74a9f1cbd8ab420a9763f0f5fabe24f Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:09:46 -0400 Subject: [PATCH 107/121] Wrap Widget objects in a COM-safe wrapper (#2620) --- .../ComSafeWidgetObjects/ComSafeWidget.cs | 356 ++++++++++++++++++ .../Helpers/WidgetHelpers.cs | 3 +- .../Services/IWidgetHostingService.cs | 2 + .../Services/WidgetHostingService.cs | 28 ++ .../ViewModels/WidgetViewModel.cs | 13 +- .../Views/AddWidgetDialog.xaml.cs | 18 +- .../Views/DashboardView.xaml.cs | 129 +++++-- 7 files changed, 509 insertions(+), 40 deletions(-) create mode 100644 tools/Dashboard/DevHome.Dashboard/ComSafeWidgetObjects/ComSafeWidget.cs diff --git a/tools/Dashboard/DevHome.Dashboard/ComSafeWidgetObjects/ComSafeWidget.cs b/tools/Dashboard/DevHome.Dashboard/ComSafeWidgetObjects/ComSafeWidget.cs new file mode 100644 index 0000000000..04e66fb717 --- /dev/null +++ b/tools/Dashboard/DevHome.Dashboard/ComSafeWidgetObjects/ComSafeWidget.cs @@ -0,0 +1,356 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using DevHome.Common.Extensions; +using DevHome.Dashboard.Services; +using Microsoft.UI.Xaml; +using Microsoft.Windows.Widgets; +using Microsoft.Windows.Widgets.Hosts; +using Serilog; +using Windows.Foundation; + +namespace DevHome.Dashboard.ComSafeWidgetObjects; + +/// +/// Since Widgets are OOP COM objects, we need to wrap them in a safe way to handle COM exceptions +/// that arise when the underlying OOP object vanishes. All Widgets should be wrapped in a +/// ComSafeWidget and calls to the widget should be done through the ComSafeWidget. +/// This class will handle the COM exceptions and get a new OOP Widget if needed. +/// All APIs on the IWidget and IWidget2 interfaces are reflected here. +/// +public class ComSafeWidget +{ + // Not currently used. + public DateTimeOffset DataLastUpdated => throw new NotImplementedException(); + + public string DefinitionId { get; private set; } + + public string Id { get; private set; } + + // Not currently used. + public DateTimeOffset TemplateLastUpdated => throw new NotImplementedException(); + + private Widget _oopWidget; + + private const int RpcServerUnavailable = unchecked((int)0x800706BA); + private const int RpcCallFailed = unchecked((int)0x800706BE); + + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ComSafeWidget)); + + private bool _hasValidProperties; + private const int MaxAttempts = 3; + + public ComSafeWidget(string widgetId) + { + Id = widgetId; + } + + public event TypedEventHandler WidgetUpdated = (_, _) => { }; + + private void OopWidgetUpdated(Widget sender, WidgetUpdatedEventArgs args) + { + WidgetUpdated.Invoke(this, args); + } + + /// + /// Gets the card template from the widget. Tries multiple times in case of COM exceptions. + /// + /// The card template, or empty JSON in the case of failure. + public async Task GetCardTemplateAsync() + { + var attempt = 0; + while (attempt++ < MaxAttempts) + { + try + { + await LazilyLoadOopWidget(); + return await Task.Run(async () => await _oopWidget.GetCardTemplateAsync()); + } + catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) + { + _log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + await GetNewOopWidgetAsync(); + } + catch (Exception ex) + { + _log.Error(ex, "Exception getting card template from widget:"); + return "{}"; + } + } + + return "{}"; + } + + /// + /// Gets the card data from the widget. Tries multiple times in case of COM exceptions. + /// + /// The card data, or empty JSON in case of failure. + public async Task GetCardDataAsync() + { + var attempt = 0; + while (attempt++ < MaxAttempts) + { + try + { + await LazilyLoadOopWidget(); + return await Task.Run(async () => await _oopWidget.GetCardDataAsync()); + } + catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) + { + _log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + await GetNewOopWidgetAsync(); + } + catch (Exception ex) + { + _log.Error(ex, "Exception getting card template from widget:"); + return "{}"; + } + } + + return "{}"; + } + + public async Task GetCustomStateAsync() + { + var attempt = 0; + while (attempt++ < MaxAttempts) + { + try + { + await LazilyLoadOopWidget(); + return await Task.Run(async () => await _oopWidget.GetCustomStateAsync()); + } + catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) + { + _log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + await GetNewOopWidgetAsync(); + } + catch (Exception ex) + { + _log.Error(ex, "Exception getting custom state from widget:"); + return string.Empty; + } + } + + return string.Empty; + } + + /// + /// Gets the size of the widget. Tries multiple times in case of COM exceptions. + /// + /// The size of the widget. Returns WidgetSize.Medium in the case of failure. + public async Task GetSizeAsync() + { + var attempt = 0; + while (attempt++ < MaxAttempts) + { + try + { + await LazilyLoadOopWidget(); + return await Task.Run(async () => await _oopWidget.GetSizeAsync()); + } + catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) + { + _log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + await GetNewOopWidgetAsync(); + } + catch (Exception ex) + { + _log.Error(ex, "Exception getting size from widget:"); + return WidgetSize.Medium; + } + } + + return WidgetSize.Medium; + } + + public async Task NotifyActionInvokedAsync(string verb, string data) + { + var attempt = 0; + while (attempt++ < MaxAttempts) + { + try + { + await LazilyLoadOopWidget(); + await Task.Run(async () => await _oopWidget.NotifyActionInvokedAsync(verb, data)); + return; + } + catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) + { + _log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + await GetNewOopWidgetAsync(); + } + catch (Exception ex) + { + _log.Error(ex, "Exception calling NotifyActionInvokedAsync on widget:"); + return; + } + } + } + + public async Task DeleteAsync() + { + var attempt = 0; + while (attempt++ < MaxAttempts) + { + try + { + await LazilyLoadOopWidget(); + await Task.Run(async () => await _oopWidget.DeleteAsync()); + return; + } + catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) + { + _log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + await GetNewOopWidgetAsync(); + } + catch (Exception ex) + { + _log.Error(ex, "Exception deleting widget:"); + return; + } + } + } + + public async Task SetCustomStateAsync(string state) + { + var attempt = 0; + while (attempt++ < MaxAttempts) + { + try + { + await LazilyLoadOopWidget(); + await Task.Run(async () => await _oopWidget.SetCustomStateAsync(state)); + return; + } + catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) + { + _log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + await GetNewOopWidgetAsync(); + } + catch (Exception ex) + { + _log.Error(ex, "Exception setting custom state on widget:"); + return; + } + } + } + + public async Task SetSizeAsync(WidgetSize widgetSize) + { + var attempt = 0; + while (attempt++ < MaxAttempts) + { + try + { + await LazilyLoadOopWidget(); + await Task.Run(async () => await _oopWidget.SetSizeAsync(widgetSize)); + return; + } + catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) + { + _log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + await GetNewOopWidgetAsync(); + } + catch (Exception ex) + { + _log.Error(ex, "Exception setting size on widget:"); + return; + } + } + } + + public async Task NotifyCustomizationRequestedAsync() + { + var attempt = 0; + while (attempt++ < MaxAttempts) + { + try + { + await LazilyLoadOopWidget(); + await Task.Run(async () => await _oopWidget.NotifyCustomizationRequestedAsync()); + return; + } + catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) + { + _log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + await GetNewOopWidgetAsync(); + } + catch (Exception ex) + { + _log.Error(ex, "Exception notifying customization requested on widget:"); + return; + } + } + } + + // Not currently used. + public IAsyncAction NotifyAnalyticsInfoAsync(string analyticsInfo) => throw new NotImplementedException(); + + // Not currently used. + public IAsyncAction NotifyErrorInfoAsync(string errorInfo) => throw new NotImplementedException(); + + private async Task GetNewOopWidgetAsync() + { + _oopWidget = null; + _hasValidProperties = false; + await LazilyLoadOopWidget(); + } + + private async Task LazilyLoadOopWidget() + { + var attempt = 0; + while (attempt++ < 3 && (_oopWidget == null || _hasValidProperties == false)) + { + try + { + _oopWidget ??= await Application.Current.GetService().GetWidgetAsync(Id); + + if (!_hasValidProperties) + { + await Task.Run(() => + { + DefinitionId = _oopWidget.DefinitionId; + Id = _oopWidget.Id; + _oopWidget.WidgetUpdated += OopWidgetUpdated; + _hasValidProperties = true; + }); + } + } + catch (Exception ex) + { + _log.Warning(ex, "Failed to get properties of out-of-proc object"); + } + } + } + + /// + /// Get a widget's ID from a widget object. Tries multiple times in case of COM exceptions. + /// + /// Widget + /// The Widget's Id, or in the case of failure string.Empty + public static async Task GetIdFromUnsafeWidgetAsync(Widget widget) + { + var retries = 5; + + return await Task.Run(() => + { + while (retries-- > 0) + { + try + { + return widget.Id; + } + catch (Exception ex) + { + Log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}, try {retries} more times"); + } + } + + return string.Empty; + }); + } +} diff --git a/tools/Dashboard/DevHome.Dashboard/Helpers/WidgetHelpers.cs b/tools/Dashboard/DevHome.Dashboard/Helpers/WidgetHelpers.cs index de17d45056..6e272277f1 100644 --- a/tools/Dashboard/DevHome.Dashboard/Helpers/WidgetHelpers.cs +++ b/tools/Dashboard/DevHome.Dashboard/Helpers/WidgetHelpers.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using DevHome.Common.Extensions; using DevHome.Common.Services; +using DevHome.Dashboard.ComSafeWidgetObjects; using Microsoft.UI.Xaml; using Microsoft.Windows.Widgets; using Microsoft.Windows.Widgets.Hosts; @@ -123,7 +124,7 @@ public static string CreateWidgetCustomState(int ordinal) return JsonSerializer.Serialize(state, SourceGenerationContext.Default.WidgetCustomState); } - public static async Task SetPositionCustomStateAsync(Widget widget, int ordinal) + public static async Task SetPositionCustomStateAsync(ComSafeWidget widget, int ordinal) { var stateStr = await widget.GetCustomStateAsync(); var state = JsonSerializer.Deserialize(stateStr, SourceGenerationContext.Default.WidgetCustomState); diff --git a/tools/Dashboard/DevHome.Dashboard/Services/IWidgetHostingService.cs b/tools/Dashboard/DevHome.Dashboard/Services/IWidgetHostingService.cs index 1da1b0682d..4abcd16923 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/IWidgetHostingService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/IWidgetHostingService.cs @@ -11,6 +11,8 @@ public interface IWidgetHostingService { public Task GetWidgetsAsync(); + public Task GetWidgetAsync(string widgetId); + public Task CreateWidgetAsync(string widgetDefinitionId, WidgetSize widgetSize); public Task GetWidgetCatalogAsync(); diff --git a/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs b/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs index 34e22f1d99..b8dfef6826 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs @@ -55,6 +55,34 @@ public async Task GetWidgetsAsync() return null; } + /// Gets the widget with the given ID. + /// The widget, or null if one could not be retrieved. + public async Task GetWidgetAsync(string widgetId) + { + var attempt = 0; + while (attempt++ < MaxAttempts) + { + try + { + _widgetHost ??= await Task.Run(() => WidgetHost.Register(new WidgetHostContext("BAA93438-9B07-4554-AD09-7ACCD7D4F031"))); + return await Task.Run(() => _widgetHost.GetWidget(widgetId)); + } + catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) + { + _log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + + _widgetHost = null; + _widgetCatalog = null; + } + catch (Exception ex) + { + _log.Error(ex, $"Exception getting widget with id {widgetId}:"); + } + } + + return null; + } + /// /// Create and return a new widget. /// diff --git a/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs b/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs index a5197d6f9b..8e2ba72257 100644 --- a/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs +++ b/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs @@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using DevHome.Common.Renderers; using DevHome.Common.Services; +using DevHome.Dashboard.ComSafeWidgetObjects; using DevHome.Dashboard.Services; using DevHome.Dashboard.TelemetryEvents; using DevHome.Telemetry; @@ -32,7 +33,7 @@ namespace DevHome.Dashboard.ViewModels; /// WidgetDefinition /// Widget view model public delegate WidgetViewModel WidgetViewModelFactory( - Widget widget, + ComSafeWidget widget, WidgetSize widgetSize, WidgetDefinition widgetDefinition); @@ -46,7 +47,7 @@ public partial class WidgetViewModel : ObservableObject private RenderedAdaptiveCard _renderedCard; [ObservableProperty] - private Widget _widget; + private ComSafeWidget _widget; [ObservableProperty] private WidgetDefinition _widgetDefinition; @@ -66,7 +67,7 @@ public partial class WidgetViewModel : ObservableObject [ObservableProperty] private FrameworkElement _widgetFrameworkElement; - partial void OnWidgetChanging(Widget value) + partial void OnWidgetChanging(ComSafeWidget value) { if (Widget != null) { @@ -74,7 +75,7 @@ partial void OnWidgetChanging(Widget value) } } - partial void OnWidgetChanged(Widget value) + partial void OnWidgetChanged(ComSafeWidget value) { if (Widget != null) { @@ -94,7 +95,7 @@ partial void OnWidgetDefinitionChanged(WidgetDefinition value) } public WidgetViewModel( - Widget widget, + ComSafeWidget widget, WidgetSize widgetSize, WidgetDefinition widgetDefinition, WidgetAdaptiveCardRenderingService adaptiveCardRenderingService, @@ -329,7 +330,7 @@ private async void HandleAdaptiveAction(RenderedAdaptiveCard sender, AdaptiveAct // https://github.com/microsoft/devhome/issues/644 } - private async void HandleWidgetUpdated(Widget sender, WidgetUpdatedEventArgs args) + private async void HandleWidgetUpdated(ComSafeWidget sender, WidgetUpdatedEventArgs args) { _log.Debug($"HandleWidgetUpdated for widget {sender.Id}"); await RenderWidgetFrameworkElementAsync(); diff --git a/tools/Dashboard/DevHome.Dashboard/Views/AddWidgetDialog.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Views/AddWidgetDialog.xaml.cs index 98b1cf6b30..750682027c 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/AddWidgetDialog.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Views/AddWidgetDialog.xaml.cs @@ -2,11 +2,13 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; using DevHome.Common.Extensions; using DevHome.Contracts.Services; +using DevHome.Dashboard.ComSafeWidgetObjects; using DevHome.Dashboard.Helpers; using DevHome.Dashboard.Services; using DevHome.Dashboard.ViewModels; @@ -83,7 +85,17 @@ private async Task FillAvailableWidgetsAsync() // Fill NavigationView Menu with Widget Providers, and group widgets under each provider. // Tag each item with the widget or provider definition, so that it can be used to create // the widget if it is selected later. - var currentlyPinnedWidgets = await _hostingService.GetWidgetsAsync(); + var unsafeCurrentlyPinnedWidgets = await _hostingService.GetWidgetsAsync(); + var comSafeCurrentlyPinnedWidgets = new List(); + foreach (var unsafeWidget in unsafeCurrentlyPinnedWidgets) + { + var id = await ComSafeWidget.GetIdFromUnsafeWidgetAsync(unsafeWidget); + if (!string.IsNullOrEmpty(id)) + { + comSafeCurrentlyPinnedWidgets.Add(new ComSafeWidget(id)); + } + } + foreach (var providerDef in providerDefinitions) { if (await WidgetHelpers.IsIncludedWidgetProviderAsync(providerDef)) @@ -100,7 +112,7 @@ private async Task FillAvailableWidgetsAsync() if (widgetDef.ProviderDefinition.Id.Equals(providerDef.Id, StringComparison.Ordinal)) { var subItemContent = await BuildWidgetNavItem(widgetDef); - var enable = !IsSingleInstanceAndAlreadyPinned(widgetDef, currentlyPinnedWidgets); + var enable = !IsSingleInstanceAndAlreadyPinned(widgetDef, [.. comSafeCurrentlyPinnedWidgets]); var subItem = new NavigationViewItem { Tag = widgetDef, @@ -168,7 +180,7 @@ private StackPanel BuildNavItem(BitmapImage image, string text) return itemContent; } - private bool IsSingleInstanceAndAlreadyPinned(WidgetDefinition widgetDef, Widget[] currentlyPinnedWidgets) + private bool IsSingleInstanceAndAlreadyPinned(WidgetDefinition widgetDef, ComSafeWidget[] currentlyPinnedWidgets) { // If a WidgetDefinition has AllowMultiple = false, only one of that widget can be pinned at one time. if (!widgetDef.AllowMultiple) diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs index 40fafb6be9..70bcf4009e 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs @@ -14,6 +14,7 @@ using DevHome.Common.Extensions; using DevHome.Common.Helpers; using DevHome.Common.Services; +using DevHome.Dashboard.ComSafeWidgetObjects; using DevHome.Dashboard.Controls; using DevHome.Dashboard.Helpers; using DevHome.Dashboard.Services; @@ -236,26 +237,36 @@ private async Task InitializePinnedWidgetListAsync(bool isFirstDashboardRun) } } - private async Task GetPreviouslyPinnedWidgets() + private async Task GetPreviouslyPinnedWidgets() { _log.Information("Get widgets for current host"); - var hostWidgets = await ViewModel.WidgetHostingService.GetWidgetsAsync(); + var unsafeHostWidgets = await ViewModel.WidgetHostingService.GetWidgetsAsync(); - if (hostWidgets == null) + if (unsafeHostWidgets == null) { _log.Information($"Found 0 widgets for this host"); return null; } - _log.Information($"Found {hostWidgets.Length} widgets for this host"); + var comSafeHostWidgets = new List(); + foreach (var unsafeWidget in unsafeHostWidgets) + { + var id = await ComSafeWidget.GetIdFromUnsafeWidgetAsync(unsafeWidget); + if (!string.IsNullOrEmpty(id)) + { + comSafeHostWidgets.Add(new ComSafeWidget(id)); + } + } + + _log.Information($"Found {comSafeHostWidgets.Count} widgets for this host"); - return hostWidgets; + return [.. comSafeHostWidgets]; } - private async Task RestorePinnedWidgetsAsync(Widget[] hostWidgets) + private async Task RestorePinnedWidgetsAsync(ComSafeWidget[] hostWidgets) { - var restoredWidgetsWithPosition = new SortedDictionary(); - var restoredWidgetsWithoutPosition = new SortedDictionary(); + var restoredWidgetsWithPosition = new SortedDictionary(); + var restoredWidgetsWithoutPosition = new SortedDictionary(); var numUnorderedWidgets = 0; var pinnedSingleInstanceWidgets = new List(); @@ -341,9 +352,9 @@ private async Task RestorePinnedWidgetsAsync(Widget[] hostWidgets) var finalPlace = 0; foreach (var orderedWidget in restoredWidgetsWithPosition) { - var widget = orderedWidget.Value; - var size = await widget.GetSizeAsync(); - await InsertWidgetInPinnedWidgetsAsync(widget, size, finalPlace++); + var comSafeWidget = orderedWidget.Value; + var size = await comSafeWidget.GetSizeAsync(); + await InsertWidgetInPinnedWidgetsAsync(comSafeWidget, size, finalPlace++); } // Go through the newly created list of pinned widgets and update any positions that may have changed. @@ -356,7 +367,7 @@ private async Task RestorePinnedWidgetsAsync(Widget[] hostWidgets) } } - private async Task DeleteAbandonedWidgetAsync(Widget widget) + private async Task DeleteAbandonedWidgetAsync(ComSafeWidget widget) { var widgetList = await ViewModel.WidgetHostingService.GetWidgetsAsync(); var length = widgetList.Length; @@ -390,19 +401,45 @@ private async Task PinDefaultWidgetAsync(WidgetDefinition defaultWidgetDefinitio { // Create widget var size = WidgetHelpers.GetDefaultWidgetSize(defaultWidgetDefinition.GetWidgetCapabilities()); - var id = defaultWidgetDefinition.Id; - var newWidget = await ViewModel.WidgetHostingService.CreateWidgetAsync(id, size); - _log.Information($"Created default widget {id}"); + var definitionId = defaultWidgetDefinition.Id; + var unsafeWidget = await ViewModel.WidgetHostingService.CreateWidgetAsync(definitionId, size); + if (unsafeWidget == null) + { + // Fail silently, since this is only the default widget and not a response to user action. + return; + } + + var unsafeWidgetId = await ComSafeWidget.GetIdFromUnsafeWidgetAsync(unsafeWidget); + if (unsafeWidgetId == string.Empty) + { + _log.Error($"Couldn't get Widget.Id, can't create the widget"); + + // If we created the widget but can't get a ComSafeWidget and show it, delete the widget. + // Again, we can fail silently since this isn't in response to user action. + try + { + await unsafeWidget.DeleteAsync(); + } + catch (Exception) + { + _log.Error($"Error deleting the widget that we couldn't create a ComSafeWidget for"); + } + + return; + } + + var comSafeWidget = new ComSafeWidget(unsafeWidgetId); + _log.Information($"Created default widget {unsafeWidgetId}"); // Set custom state on new widget. var position = PinnedWidgets.Count; var newCustomState = WidgetHelpers.CreateWidgetCustomState(position); _log.Debug($"SetCustomState: {newCustomState}"); - await newWidget.SetCustomStateAsync(newCustomState); + await comSafeWidget.SetCustomStateAsync(newCustomState); // Put new widget on the Dashboard. - await InsertWidgetInPinnedWidgetsAsync(newWidget, size, position); - _log.Information($"Inserted default widget {id} at position {position}"); + await InsertWidgetInPinnedWidgetsAsync(comSafeWidget, size, position); + _log.Information($"Inserted default widget {unsafeWidgetId} at position {position}"); } catch (Exception ex) { @@ -438,35 +475,67 @@ public async Task AddWidgetClickAsync() if (newWidgetDefinition != null) { - Widget newWidget; try { var size = WidgetHelpers.GetDefaultWidgetSize(newWidgetDefinition.GetWidgetCapabilities()); - newWidget = await ViewModel.WidgetHostingService.CreateWidgetAsync(newWidgetDefinition.Id, size); + var unsafeWidget = await ViewModel.WidgetHostingService.CreateWidgetAsync(newWidgetDefinition.Id, size); + if (unsafeWidget == null) + { + // Couldn't create the widget, show an error message. + await ShowCreateWidgetErrorMessage(); + return; + } + + var unsafeWidgetId = await ComSafeWidget.GetIdFromUnsafeWidgetAsync(unsafeWidget); + if (unsafeWidgetId == string.Empty) + { + _log.Error($"Couldn't get Widget.Id, can't create the widget"); + await ShowCreateWidgetErrorMessage(); + + // If we created the widget but can't get a ComSafeWidget and show it, delete the widget. + // We can try and catch silently, since the user already saw an error that the widget couldn't be created. + try + { + await unsafeWidget.DeleteAsync(); + } + catch (Exception) + { + _log.Error($"Error deleting the widget that we couldn't create a ComSafeWidget for"); + } + + return; + } + + var comSafeWidget = new ComSafeWidget(unsafeWidgetId); // Set custom state on new widget. var position = PinnedWidgets.Count; var newCustomState = WidgetHelpers.CreateWidgetCustomState(position); _log.Debug($"SetCustomState: {newCustomState}"); - await newWidget.SetCustomStateAsync(newCustomState); + await comSafeWidget.SetCustomStateAsync(newCustomState); // Put new widget on the Dashboard. - await InsertWidgetInPinnedWidgetsAsync(newWidget, size, position); + await InsertWidgetInPinnedWidgetsAsync(comSafeWidget, size, position); } catch (Exception ex) { _log.Warning(ex, $"Creating widget failed: "); - var mainWindow = Application.Current.GetService(); - var stringResource = new StringResource("DevHome.Dashboard.pri", "DevHome.Dashboard/Resources"); - await mainWindow.ShowErrorMessageDialogAsync( - title: string.Empty, - content: stringResource.GetLocalized("CouldNotCreateWidgetError"), - buttonText: stringResource.GetLocalized("CloseButtonText")); + await ShowCreateWidgetErrorMessage(); } } } - private async Task InsertWidgetInPinnedWidgetsAsync(Widget widget, WidgetSize size, int index) + private async Task ShowCreateWidgetErrorMessage() + { + var mainWindow = Application.Current.GetService(); + var stringResource = new StringResource("DevHome.Dashboard.pri", "DevHome.Dashboard/Resources"); + await mainWindow.ShowErrorMessageDialogAsync( + title: string.Empty, + content: stringResource.GetLocalized("CouldNotCreateWidgetError"), + buttonText: stringResource.GetLocalized("CloseButtonText")); + } + + private async Task InsertWidgetInPinnedWidgetsAsync(ComSafeWidget widget, WidgetSize size, int index) { await Task.Run(async () => { @@ -505,7 +574,7 @@ await Task.Run(async () => }); } - private async Task DeleteWidgetWithNoDefinition(Widget widget, string widgetDefinitionId) + private async Task DeleteWidgetWithNoDefinition(ComSafeWidget widget, string widgetDefinitionId) { // If the widget provider was uninstalled while we weren't running, the catalog won't have the definition so delete the widget. _log.Information($"No widget definition '{widgetDefinitionId}', delete widget with that definition"); From 887850f6fbf91d2a7aebf2dfc0a2843115d0ccc6 Mon Sep 17 00:00:00 2001 From: Jeff Whiteside Date: Tue, 16 Apr 2024 17:49:10 -0700 Subject: [PATCH 108/121] Add 'Analytic Summary' & PerformanceRecorderEngine to Quiet Background Processes feature (#2596) * Move Quiet Background Processes UI to its own project * Add 'Analytic Summary' & PerformanceRecorderEngine to Quiet Background Processes feature * When starting a quiet session, also start a performance recording session * Added Analytic Summary with process performance table (incl. filtering and sorting) * Displays gathered metrics from processes running during a quiet session * Add usage telemetry * Updated icon & strings from Content UX session * Make it visible in Canary builds * Iteration 4 - Addressed PR comments * Make ContentDiaglog use standard buttons * ExtensionWrapper bug * Move style * Make visible * Fix strings * Fix after rebase * remove references to renamed SimpleTableControl * Remove unused usings * whitespace * * Store results to a .dat file on disk if DevHome isn't running to received them * Iteration 15 comments from Kristen * Fix empty button issue * Fix potential crash --------- Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com> --- DevHome.sln | 38 + .../Strings/en-us/Resources.resw | 8 +- src/DevHome.csproj | 5 +- src/NavConfig.jsonc | 8 +- src/Package.appxmanifest | 1 + .../DevHome.Customization.csproj | 6 +- .../Extensions/ServiceExtensions.cs | 5 +- .../Strings/en-us/Resources.resw | 52 -- .../Views/MainPageView.xaml | 3 +- .../Common.h | 10 + ...me.QuietBackgroundProcesses.Common.vcxproj | 1 + .../DevHome.QuietBackgroundProcesses.idl | 55 +- ...BackgroundProcesses.ElevatedServer.vcxproj | 10 +- .../Helpers.cpp | 46 ++ .../Helpers.h | 43 ++ .../PerformanceRecorderEngineWinrt.cpp | 346 +++++++++ .../QuietBackgroundProcessesSession.cpp | 8 +- .../TimedQuietSession.h | 25 +- ...rocesses.PerformanceRecorderEngine.vcxproj | 188 +++++ ....PerformanceRecorderEngine.vcxproj.filters | 29 + .../PerformanceRecorderEngine.cpp | 674 ++++++++++++++++++ .../PerformanceRecorderEngine.h | 38 + .../dllmain.cpp | 17 + .../packages.config | 4 + .../pch.cpp | 4 + .../pch.h | 10 + ...QuietBackgroundProcessesSessionManager.cpp | 21 + ...DevHome.QuietBackgroundProcesses.UI.csproj | 38 + .../ProcessData.cs | 58 ++ .../QuietBackgroundProcessesEvent.cs | 28 + .../Strings/en-us/Resources.resw | 179 +++++ .../AnalyticSummaryPopupViewModel.cs | 199 ++++++ .../QuietBackgroundProcessesViewModel.cs | 95 ++- .../Views/AnalyticSummaryPopup.xaml | 81 +++ .../Views/AnalyticSummaryPopup.xaml.cs | 44 ++ .../Views/ProcessPerformanceTableControl.xaml | 107 +++ .../ProcessPerformanceTableControl.xaml.cs | 31 + .../Views/QuietBackgroundProcessesView.xaml | 18 +- .../QuietBackgroundProcessesView.xaml.cs | 13 +- 39 files changed, 2448 insertions(+), 98 deletions(-) create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/Common.h create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.h create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/PerformanceRecorderEngineWinrt.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj.filters create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.h create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/dllmain.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/packages.config create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.cpp create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.h create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/DevHome.QuietBackgroundProcesses.UI.csproj create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ProcessData.cs create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/QuietBackgroundProcessesEvent.cs create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/AnalyticSummaryPopupViewModel.cs rename tools/{Customization/DevHome.Customization => QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI}/ViewModels/QuietBackgroundProcessesViewModel.cs (66%) create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml.cs create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml create mode 100644 tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml.cs rename tools/{Customization/DevHome.Customization => QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI}/Views/QuietBackgroundProcessesView.xaml (72%) rename tools/{Customization/DevHome.Customization => QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI}/Views/QuietBackgroundProcessesView.xaml.cs (55%) diff --git a/DevHome.sln b/DevHome.sln index b842116a53..bb03d89b0a 100644 --- a/DevHome.sln +++ b/DevHome.sln @@ -80,6 +80,10 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DevHome.QuietBackgroundProc EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DevHome.QuietBackgroundProcesses.Common", "tools\QuietBackgroundProcesses\DevHome.QuietBackgroundProcesses.Common\DevHome.QuietBackgroundProcesses.Common.vcxproj", "{4B370E2F-FB1D-4887-90BF-3B72517485CE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.QuietBackgroundProcesses.UI", "tools\QuietBackgroundProcesses\DevHome.QuietBackgroundProcesses.UI\DevHome.QuietBackgroundProcesses.UI.csproj", "{1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine", "tools\QuietBackgroundProcesses\DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine\DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj", "{AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{DCAF188B-60C3-4EDB-8049-BAA927FBCD7D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SampleTool", "SampleTool", "{E7C94F61-D6CF-464D-8D50-210488AF7A50}" @@ -516,6 +520,22 @@ Global {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|x64.Build.0 = Release|x64 {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|x86.ActiveCfg = Release|Win32 {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|x86.Build.0 = Release|Win32 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|Any CPU.ActiveCfg = Debug|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|Any CPU.Build.0 = Debug|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|arm64.ActiveCfg = Debug|arm64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|arm64.Build.0 = Debug|arm64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|x64.ActiveCfg = Debug|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|x64.Build.0 = Debug|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|x86.ActiveCfg = Debug|x86 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|x86.Build.0 = Debug|x86 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|Any CPU.ActiveCfg = Release|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|Any CPU.Build.0 = Release|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|arm64.ActiveCfg = Release|arm64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|arm64.Build.0 = Release|arm64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|x64.ActiveCfg = Release|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|x64.Build.0 = Release|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|x86.ActiveCfg = Release|x86 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|x86.Build.0 = Release|x86 {CFD8A90D-8B6D-4ED6-BA35-FF894BEB46C0}.Debug|Any CPU.ActiveCfg = Debug|x64 {CFD8A90D-8B6D-4ED6-BA35-FF894BEB46C0}.Debug|Any CPU.Build.0 = Debug|x64 {CFD8A90D-8B6D-4ED6-BA35-FF894BEB46C0}.Debug|arm64.ActiveCfg = Debug|ARM64 @@ -740,6 +760,22 @@ Global {AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}.Release|x64.Build.0 = Release|x64 {AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}.Release|x86.ActiveCfg = Release|x86 {AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}.Release|x86.Build.0 = Release|x86 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|Any CPU.ActiveCfg = Debug|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|Any CPU.Build.0 = Debug|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|arm64.ActiveCfg = Debug|ARM64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|arm64.Build.0 = Debug|ARM64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|x64.ActiveCfg = Debug|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|x64.Build.0 = Debug|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|x86.ActiveCfg = Debug|Win32 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|x86.Build.0 = Debug|Win32 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|Any CPU.ActiveCfg = Release|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|Any CPU.Build.0 = Release|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|arm64.ActiveCfg = Release|ARM64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|arm64.Build.0 = Release|ARM64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|x64.ActiveCfg = Release|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|x64.Build.0 = Release|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|x86.ActiveCfg = Release|Win32 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -767,6 +803,8 @@ Global {092AC740-DA01-4872-8E93-B9557DAD6BE5} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} {80805B43-CE75-4C6E-92F8-F385C1039E53} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} {4B370E2F-FB1D-4887-90BF-3B72517485CE} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} {E7C94F61-D6CF-464D-8D50-210488AF7A50} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} {8FC9A04E-1FFD-42BA-B304-D1FA964D99CE} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} {CFD8A90D-8B6D-4ED6-BA35-FF894BEB46C0} = {8FC9A04E-1FFD-42BA-B304-D1FA964D99CE} diff --git a/settings/DevHome.Settings/Strings/en-us/Resources.resw b/settings/DevHome.Settings/Strings/en-us/Resources.resw index d73581bd82..b078587296 100644 --- a/settings/DevHome.Settings/Strings/en-us/Resources.resw +++ b/settings/DevHome.Settings/Strings/en-us/Resources.resw @@ -559,12 +559,12 @@ Title text for the Environments configuration feature. - Quiet background processes experiment - Name of experimental feature ()'Quiet Background Processes') on the 'Settings -> Experiments' page where you enable it. + Quiet background processes + Name of experimental feature 'Quiet background processes' on the 'Settings -> Experiments' page where you enable it. - Silence and track background processes that may hinder device performance - Inline description of the Quiet Background Processes experimental feature on the 'Settings -> Experiments' page where you enable it. + Quiet background processes allows you to free up resources while developing + Inline description of the Quiet background processes experimental feature on the 'Settings -> Experiments' page where you enable it. Create a local or cloud machine from Dev Home diff --git a/src/DevHome.csproj b/src/DevHome.csproj index d749292754..039b6a841d 100644 --- a/src/DevHome.csproj +++ b/src/DevHome.csproj @@ -119,7 +119,7 @@ true - + <_DevHomeInternal_CppPlatform>$(Platform) <_DevHomeInternal_CppPlatform Condition="'$(Platform)' == 'x86'">Win32 @@ -138,6 +138,9 @@ PreserveNewest + + PreserveNewest + diff --git a/src/NavConfig.jsonc b/src/NavConfig.jsonc index 8aa0a372e2..9689941e21 100644 --- a/src/NavConfig.jsonc +++ b/src/NavConfig.jsonc @@ -86,18 +86,18 @@ "buildTypeOverrides": [ { "buildType": "dev", - "enabledByDefault": false, + "enabledByDefault": true, "visible": true }, { "buildType": "canary", - "enabledByDefault": false, - "visible": false + "enabledByDefault": true, + "visible": true }, { "buildType": "stable", "enabledByDefault": false, - "visible": false + "visible": true } ] }, diff --git a/src/Package.appxmanifest b/src/Package.appxmanifest index 3e44bb0bc3..0e9dc3da0a 100644 --- a/src/Package.appxmanifest +++ b/src/Package.appxmanifest @@ -22,6 +22,7 @@ DevHome.QuietBackgroundProcesses.ElevatedServer.exe singleInstance + diff --git a/tools/Customization/DevHome.Customization/DevHome.Customization.csproj b/tools/Customization/DevHome.Customization/DevHome.Customization.csproj index be0924d636..d06a565d6b 100644 --- a/tools/Customization/DevHome.Customization/DevHome.Customization.csproj +++ b/tools/Customization/DevHome.Customization/DevHome.Customization.csproj @@ -24,9 +24,9 @@ + - @@ -40,7 +40,6 @@ - @@ -60,9 +59,6 @@ MSBuild:Compile $(DefaultXamlRuntime) - - $(DefaultXamlRuntime) - diff --git a/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs b/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs index e3788796c5..1372f2b018 100644 --- a/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs +++ b/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs @@ -4,6 +4,7 @@ using DevHome.Customization.ViewModels; using DevHome.Customization.ViewModels.DevDriveInsights; using DevHome.Customization.Views; +using DevHome.QuietBackgroundProcesses.UI.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -19,7 +20,9 @@ public static IServiceCollection AddWindowsCustomization(this IServiceCollection services.AddSingleton(); services.AddTransient(); - services.AddSingleton(sp => (cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters) => ActivatorUtilities.CreateInstance(sp, cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters)); + services.AddSingleton(sp => + (cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters) => + ActivatorUtilities.CreateInstance(sp, cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters)); services.AddSingleton(); services.AddTransient(); diff --git a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw index 8c4f0d8428..ce4d1ddcbd 100644 --- a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw +++ b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw @@ -249,56 +249,4 @@ Windows developer settings The header for the Windows developer settings card - - - - Start session - Button that starts a quiet background session - - - Stop session - Button that stops a quiet background session - - - Silence and track background processes that may hinder device performance - Description of the Quiet Background Processes feature - - - Quiet background processes - Inline title of the Quiet Background Processes feature - - - This feature can be activated for 2 hours. - A description of the Quiet Background Processes time window - - - Link to docs - Link that launches documentation - - - Provide feedback - Link that launches feedback - - - Related links - Label for the doc links - - - - - Feature not supported on this version of Windows - Indicates that this OS isn't new enough to support the feature - - - Session Error - Something went wrong when running the session - - - Session ended - The quiet session was cancelled or the time expired - - - Unable to cancel session - Something went wrong when cancelling the session - \ No newline at end of file diff --git a/tools/Customization/DevHome.Customization/Views/MainPageView.xaml b/tools/Customization/DevHome.Customization/Views/MainPageView.xaml index cb805cb118..9e957a96f2 100644 --- a/tools/Customization/DevHome.Customization/Views/MainPageView.xaml +++ b/tools/Customization/DevHome.Customization/Views/MainPageView.xaml @@ -6,6 +6,7 @@ xmlns:converters="using:CommunityToolkit.WinUI.Converters" xmlns:ui="using:CommunityToolkit.WinUI" xmlns:views="using:DevHome.Customization.Views" + xmlns:quietviews="using:DevHome.QuietBackgroundProcesses.UI.Views" Loaded="UserControl_Loaded"> @@ -39,7 +40,7 @@ - + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/Common.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/Common.h new file mode 100644 index 0000000000..e92ab65a15 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/Common.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +// Get temporary path for performance data +inline std::filesystem::path GetTemporaryPerformanceDataPath() +{ + auto tempDirectory = std::filesystem::temp_directory_path(); + return std::filesystem::path(tempDirectory) / L"DevHome.QuietMode.PerformanceData.dat"; +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.Common.vcxproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.Common.vcxproj index 54ea440e67..b790816874 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.Common.vcxproj +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.Common.vcxproj @@ -145,6 +145,7 @@ + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.idl b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.idl index c64cb8d568..26e7ead7da 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.idl +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.idl @@ -2,15 +2,66 @@ // Licensed under the MIT License. import "inspectable.idl"; +import "windows.foundation.idl"; namespace DevHome.QuietBackgroundProcesses { + // Performance Engine types + enum ProcessCategory + { + Unknown, + User, + System, + Developer, + Background, + }; + + [default_interface] + runtimeclass ProcessRow + { + UInt32 Pid { get; }; + String Name { get; }; + String PackageFullName { get; }; + String Aumid { get; }; + String Path { get; }; + ProcessCategory Category { get; }; + Windows.Foundation.DateTime CreateTime { get; }; + Windows.Foundation.DateTime ExitTime { get; }; + + UInt64 SampleCount { get; }; + Double PercentCumulative { get; }; + Double VarianceCumulative { get; }; + Double Sigma4Cumulative { get; }; + Double MaxPercent { get; }; + UInt32 SamplesAboveThreshold { get; }; + + UInt64 TotalCpuTimeInMicroseconds { get; }; + } + + [default_interface] + runtimeclass ProcessPerformanceTable + { + ProcessRow[] Rows { get; }; + } + + [default_interface] + runtimeclass PerformanceRecorderEngine + { + static ProcessPerformanceTable TryGetLastPerformanceRecording(); + + PerformanceRecorderEngine(); + void Start(Windows.Foundation.TimeSpan periodInMs); + ProcessPerformanceTable Stop(); + } + + // Quiet background process types [default_interface] runtimeclass QuietBackgroundProcessesSession { static QuietBackgroundProcessesSession GetSingleton(); + Int64 Start(); - void Stop(); + ProcessPerformanceTable Stop(); Boolean IsActive { get; }; Int64 TimeLeftInSeconds { get; }; } @@ -21,5 +72,7 @@ namespace DevHome.QuietBackgroundProcesses static Boolean IsFeaturePresent(); static QuietBackgroundProcessesSession GetSession(); static QuietBackgroundProcessesSession TryGetSession(); + static Boolean HasLastPerformanceRecording(); + static ProcessPerformanceTable TryGetLastPerformanceRecording(); } } diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj index 001ddcadaa..b2a6ac18a7 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj @@ -112,8 +112,8 @@ - stdcpp17 - $(ProjectDir)..\DevHome.QuietBackgroundProcesses.Common\;$(ProjectDir)..\DevHome.QuietBackgroundProcesses.Common\$(GeneratedFilesDir)midl;%(AdditionalIncludeDirectories) + stdcpp20 + $(ProjectDir)..\DevHome.QuietBackgroundProcesses.Common\;$(ProjectDir)..\DevHome.QuietBackgroundProcesses.Common\$(GeneratedFilesDir)midl;$(ProjectDir)..\DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine\;%(AdditionalIncludeDirectories) onecore.lib;%(AdditionalDependencies) @@ -135,16 +135,19 @@ + + Create + @@ -158,6 +161,9 @@ {4b370e2f-fb1d-4887-90bf-3b72517485ce} + + {af5a7fa0-e3e8-44c8-8830-31dd08f583e8} + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.cpp new file mode 100644 index 0000000000..1fe81e8cfb --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.cpp @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#include +#include +#include +#include + +#include "PerformanceRecorderEngine.h" +#include "Helpers.h" + +void WritePerformanceDataToDisk(_In_ PCWSTR path, const std::span& data) +{ + std::ofstream file(path, std::ios::binary); + if (!file.is_open()) + { + // Handle error + return; + } + + for (const auto& item : data) + { + file.write(reinterpret_cast(&item), sizeof(ProcessPerformanceSummary)); + } + + file.close(); +} + +std::vector ReadPerformanceDataFromDisk(_In_ PCWSTR path) +{ + std::vector data; + + std::ifstream file(path, std::ios::binary); + THROW_WIN32_IF(ERROR_SHARING_VIOLATION, !file.is_open()); + + ProcessPerformanceSummary item; + while (file.read(reinterpret_cast(&item), sizeof(ProcessPerformanceSummary))) + { + data.push_back(item); + } + + file.close(); + return data; +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.h new file mode 100644 index 0000000000..9045c9ae2f --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.h @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +#include +#include + +#include "DevHome.QuietBackgroundProcesses.h" +#include "PerformanceRecorderEngine.h" + +struct com_ptr_deleter +{ + template + void operator()(_Pre_opt_valid_ _Frees_ptr_opt_ T p) const + { + if (p) + { + p.reset(); + } + } +}; + +template +using unique_comptr_array = wil::unique_any_array_ptr, ArrayDeleter, com_ptr_deleter>; + +template +unique_comptr_array make_unique_comptr_array(size_t numOfElements) +{ + auto list = unique_comptr_array(reinterpret_cast*>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, numOfElements * sizeof(wil::com_ptr_nothrow))), numOfElements); + THROW_IF_NULL_ALLOC(list.get()); + return list; +} + +// Create a performance recorder engine +wil::com_ptr MakePerformanceRecorderEngine(); + +// Read/write the performance data to/from disk +void WritePerformanceDataToDisk(_In_ PCWSTR path, const std::span& data); +std::vector ReadPerformanceDataFromDisk(_In_ PCWSTR path); diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/PerformanceRecorderEngineWinrt.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/PerformanceRecorderEngineWinrt.cpp new file mode 100644 index 0000000000..df66eff75a --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/PerformanceRecorderEngineWinrt.cpp @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "Common.h" +#include "TimedQuietSession.h" +#include "DevHome.QuietBackgroundProcesses.h" +#include "PerformanceRecorderEngine.h" +#include "Helpers.h" + + +namespace ABI::DevHome::QuietBackgroundProcesses +{ + class ProcessRow : + public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, + IProcessRow, + Microsoft::WRL::FtmBase> + { + InspectableClass(RuntimeClass_DevHome_QuietBackgroundProcesses_ProcessRow, BaseTrust); + + public: + STDMETHODIMP RuntimeClassInitialize(ProcessPerformanceSummary summary) noexcept + { + m_summary = summary; + return S_OK; + } + + STDMETHODIMP get_Pid(unsigned int* value) noexcept override + try + { + *value = m_summary.pid; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_Name(HSTRING* value) noexcept override + try + { + Microsoft::WRL::Wrappers::HString str; + str.Set(m_summary.name); + *value = str.Detach(); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_PackageFullName(HSTRING* value) noexcept override + try + { + Microsoft::WRL::Wrappers::HString str; + str.Set(m_summary.packageFullName); + *value = str.Detach(); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_Aumid(HSTRING* value) noexcept override + try + { + Microsoft::WRL::Wrappers::HString str; + str.Set(m_summary.aumid); + *value = str.Detach(); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_Path(HSTRING* value) noexcept override + try + { + Microsoft::WRL::Wrappers::HString str; + str.Set(m_summary.path); + *value = str.Detach(); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_Category(ABI::DevHome::QuietBackgroundProcesses::ProcessCategory* value) noexcept override + try + { + *value = static_cast(m_summary.category); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_CreateTime(struct ABI::Windows::Foundation::DateTime* value) noexcept override + try + { + INT64 time = m_summary.createTime.dwLowDateTime + ((UINT64)m_summary.createTime.dwHighDateTime << 32); + *value = ABI::Windows::Foundation::DateTime{ time }; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_ExitTime(struct ABI::Windows::Foundation::DateTime* value) noexcept override + try + { + INT64 time = m_summary.exitTime.dwLowDateTime + ((UINT64)m_summary.exitTime.dwHighDateTime << 32); + *value = ABI::Windows::Foundation::DateTime{ time }; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_SampleCount(unsigned __int64* value) noexcept override + try + { + *value = m_summary.sampleCount; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_PercentCumulative(double* value) noexcept override + try + { + *value = m_summary.percentCumulative; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_VarianceCumulative(double* value) noexcept override + try + { + *value = m_summary.varianceCumulative; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_Sigma4Cumulative(double* value) noexcept override + try + { + *value = m_summary.sigma4Cumulative; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_MaxPercent(double* value) noexcept override + try + { + *value = m_summary.maxPercent; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_SamplesAboveThreshold(unsigned __int32* value) noexcept override + try + { + *value = m_summary.samplesAboveThreshold; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_TotalCpuTimeInMicroseconds(unsigned __int64* value) noexcept override + try + { + *value = m_summary.totalCpuTimeInMicroseconds; + return S_OK; + } + CATCH_RETURN() + + private: + ProcessPerformanceSummary m_summary; + }; +} + +namespace ABI::DevHome::QuietBackgroundProcesses +{ + class ProcessPerformanceTable : + public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, + IProcessPerformanceTable, + Microsoft::WRL::FtmBase> + { + InspectableClass(RuntimeClass_DevHome_QuietBackgroundProcesses_ProcessPerformanceTable, BaseTrust); + + public: + STDMETHODIMP RuntimeClassInitialize(unique_process_utilization_monitoring_thread context) noexcept + { + m_context = std::move(context); + return S_OK; + } + + STDMETHODIMP get_Rows(unsigned int* valueLength, ABI::DevHome::QuietBackgroundProcesses::IProcessRow*** value) noexcept override + try + { + std::span span; + wil::unique_cotaskmem_array_ptr summariesCoarray; + std::vector summariesVector; + + if (m_context) + { + // We have a live context, read performance data from it + THROW_IF_FAILED(GetMonitoringProcessUtilization(m_context.get(), summariesCoarray.addressof(), summariesCoarray.size_address())); + + // Make span from cotaskmem_array + span = std::span{ summariesCoarray.get(), summariesCoarray.size() }; + } + else + { + // We don't have a live context. Let's try to read performance data from disk. + auto performanceDataFile = GetTemporaryPerformanceDataPath(); + THROW_HR_IF(E_FAIL, !std::filesystem::exists(performanceDataFile)); + + // Make span from vector + summariesVector = ReadPerformanceDataFromDisk(performanceDataFile.c_str()); + span = std::span{ summariesVector }; + } + + // Create IProcessRow entries + auto list = make_unique_comptr_array(span.size()); + for (uint32_t i = 0; i < span.size(); i++) + { + auto& summary = span[i]; + wil::com_ptr row; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&row, summary)); + list[i] = std::move(row); + } + *valueLength = static_cast(span.size()); + *value = (ABI::DevHome::QuietBackgroundProcesses::IProcessRow**)list.release(); + return S_OK; + } + CATCH_RETURN() + + private: + unique_process_utilization_monitoring_thread m_context; + }; +} + +namespace ABI::DevHome::QuietBackgroundProcesses +{ + class PerformanceRecorderEngine : + public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, + IPerformanceRecorderEngine, + Microsoft::WRL::FtmBase> + { + InspectableClass(RuntimeClass_DevHome_QuietBackgroundProcesses_PerformanceRecorderEngine, BaseTrust); + + public: + STDMETHODIMP RuntimeClassInitialize() noexcept + { + return S_OK; + } + + // IPerformanceRecorderEngine + STDMETHODIMP Start(ABI::Windows::Foundation::TimeSpan samplingPeriod) noexcept override + try + { + // Convert TimeSpan from 100ns to milliseconds + auto periodInMs = static_cast(samplingPeriod.Duration / 10000); + THROW_IF_FAILED(StartMonitoringProcessUtilization(periodInMs, &m_context)); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP Stop(ABI::DevHome::QuietBackgroundProcesses::IProcessPerformanceTable** result) noexcept override + try + { + THROW_IF_FAILED(StopMonitoringProcessUtilization(m_context.get())); + + if (result) + { + wil::com_ptr performanceTable; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&performanceTable, std::move(m_context))); + *result = performanceTable.detach(); + } + else + { + // No one (no client) is currently asking for the performance data (presumably Dev Home is closed) so write it to disk + wil::unique_cotaskmem_array_ptr summaries; + THROW_IF_FAILED(GetMonitoringProcessUtilization(m_context.get(), summaries.addressof(), summaries.size_address())); + + // Write the performance .csv data to disk + std::span data(summaries.get(), summaries.size()); + try + { + auto performanceDataFile = GetTemporaryPerformanceDataPath(); + WritePerformanceDataToDisk(performanceDataFile.c_str(), data); + } + CATCH_LOG(); + + // Destroy the performance engine instance + m_context.reset(); + } + + return S_OK; + } + CATCH_RETURN() + + private: + unique_process_utilization_monitoring_thread m_context; + }; + + class PerformanceRecorderEngineStatics WrlFinal : + public Microsoft::WRL::AgileActivationFactory< + Microsoft::WRL::Implements> + { + InspectableClassStatic(RuntimeClass_DevHome_QuietBackgroundProcesses_PerformanceRecorderEngine, BaseTrust); + + public: + STDMETHODIMP ActivateInstance(_COM_Outptr_ IInspectable**) noexcept + { + // Disallow activation - must use GetSingleton() + return E_NOTIMPL; + } + + // IPerformanceRecorderEngineStatics + STDMETHODIMP TryGetLastPerformanceRecording(_COM_Outptr_ ABI::DevHome::QuietBackgroundProcesses::IProcessPerformanceTable** result) noexcept override + try + { + // Reconstruct a perform table from disk (passing nullptr for context) + wil::com_ptr performanceTable; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&performanceTable, nullptr)); + *result = performanceTable.detach(); + + return S_OK; + } + CATCH_RETURN() + }; + + ActivatableClassWithFactory(PerformanceRecorderEngine, PerformanceRecorderEngineStatics); +} + +wil::com_ptr MakePerformanceRecorderEngine() +{ + using namespace ABI::DevHome::QuietBackgroundProcesses; + wil::com_ptr result; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&result)); + return result; +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietBackgroundProcessesSession.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietBackgroundProcessesSession.cpp index fbd08ff74a..f0483e7b73 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietBackgroundProcessesSession.cpp +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietBackgroundProcessesSession.cpp @@ -49,7 +49,7 @@ namespace ABI::DevHome::QuietBackgroundProcesses // Stop and discard the previous timer if (g_activeTimer) { - g_activeTimer->Cancel(); + g_activeTimer->Cancel(nullptr); } std::chrono::seconds duration = DEFAULT_QUIET_DURATION; @@ -67,14 +67,16 @@ namespace ABI::DevHome::QuietBackgroundProcesses } CATCH_RETURN() - STDMETHODIMP Stop() noexcept override try + STDMETHODIMP Stop(ABI::DevHome::QuietBackgroundProcesses::IProcessPerformanceTable** result) noexcept override + try { auto lock = std::scoped_lock(g_mutex); + *result = nullptr; // Turn off quiet mode and cancel timer if (g_activeTimer) { - g_activeTimer->Cancel(); + g_activeTimer->Cancel(result); g_activeTimer.reset(); } diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/TimedQuietSession.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/TimedQuietSession.h index 3dcc157e69..7e809cf045 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/TimedQuietSession.h +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/TimedQuietSession.h @@ -20,6 +20,7 @@ #include "Timer.h" #include "QuietState.h" +#include "Helpers.h" using ElevatedServerReference = wrl_server_process_ref; @@ -39,7 +40,7 @@ struct UnelevatedServerReference }; -// TimedQuietSession is a 2 hour "Quiet Background Processes" timed window that disables quiet +// TimedQuietSession is a 2 hour "Quiet background processes" timed window that disables quiet // mode when the timer expires or when explicitly cancelled. It keeps also keeps the server alive. // // TimedQuietSession maintains, @@ -88,6 +89,12 @@ struct TimedQuietSession // Turn on quiet mode m_quietState = QuietState::TurnOn(); + + // Start performance recorder + ABI::Windows::Foundation::TimeSpan samplingPeriod; + samplingPeriod.Duration = 1000 * 10000; // 1 second + m_performanceRecorderEngine = MakePerformanceRecorderEngine(); + THROW_IF_FAILED(m_performanceRecorderEngine->Start(samplingPeriod)); } TimedQuietSession(TimedQuietSession&& other) noexcept = default; @@ -108,11 +115,11 @@ struct TimedQuietSession return (bool)m_quietState; } - void Cancel() + void Cancel(ABI::DevHome::QuietBackgroundProcesses::IProcessPerformanceTable** result) { auto lock = std::scoped_lock(m_mutex); - Deactivate(); + Deactivate(result); m_timer->Cancel(); // Destruct timer on another thread because it's destructor is blocking @@ -124,11 +131,20 @@ struct TimedQuietSession } private: - void Deactivate() + void Deactivate(ABI::DevHome::QuietBackgroundProcesses::IProcessPerformanceTable** result = nullptr) { // Turn off quiet mode m_quietState.reset(); + // Stop the performance recorder + if (m_performanceRecorderEngine) + { + LOG_IF_FAILED(m_performanceRecorderEngine->Stop(result)); + } + + // Disable performance recorder + m_performanceRecorderEngine.reset(); + // Release lifetime handles to this elevated server and unelevated client server m_unelevatedServer.reset(); m_elevatedServer.reset(); @@ -139,5 +155,6 @@ struct TimedQuietSession QuietState::unique_quietwindowclose_call m_quietState{ false }; std::unique_ptr m_timer; + wil::com_ptr m_performanceRecorderEngine; std::mutex m_mutex; }; diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj new file mode 100644 index 0000000000..6f47427600 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj @@ -0,0 +1,188 @@ + + + + C++ + + + + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + {af5a7fa0-e3e8-44c8-8830-31dd08f583e8} + DynamicLibrary + DevHome_QuietBackgroundProcesses_PerformanceRecorderEngine + en-US + 14.0 + false + Windows Store + 10.0.22621.0 + 10.0.17134.0 + 10.0 + $(CppOutDir) + + + + DynamicLibrary + true + v143 + + + DynamicLibrary + true + v143 + + + DynamicLibrary + true + v143 + + + DynamicLibrary + false + true + v143 + + + DynamicLibrary + false + true + v143 + + + DynamicLibrary + false + true + v143 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + + Use + false + stdcpp20 + + + Console + false + false + onecore.lib;onecoreuap_apiset.lib;%(AdditionalDependencies) + + + + + _DEBUG;%(PreprocessorDefinitions) + Disabled + + + + + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + + + Create + Create + Create + Create + Create + Create + + + + + + + + + + + + + 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/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj.filters b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj.filters new file mode 100644 index 0000000000..e1d45a4ab6 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj.filters @@ -0,0 +1,29 @@ + + + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms + + + {a4747e26-49cd-412b-b5ad-2d499887d0d5} + + + + + + + + + + + inc + + + + + + + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.cpp new file mode 100644 index 0000000000..b182a1ff6f --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.cpp @@ -0,0 +1,674 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "PerformanceRecorderEngine.h" + +// 2 percent is the threshold for a process to be considered as a high CPU consumer +#define CPU_TIME_ABOVE_THRESHOLD_STRIKE_VALUE 0.02f + +enum class ProcessCategory +{ + Unknown, + User, + System, + Developer, + Background, +}; + +struct ProcessPerformanceInfo +{ + // Process id + wil::unique_process_handle process; + ULONG pid{}; + + // Process info + std::wstring name; + std::wstring path; + std::optional packageFullName; + std::optional aumid; + ProcessCategory category{}; + FILETIME createTime{}; + FILETIME exitTime{}; + + // CPU times + FILETIME startUserTime{}; + FILETIME startKernelTime{}; + FILETIME previousUserTime{}; + FILETIME currentUserTime{}; + FILETIME previousKernelTime{}; + FILETIME currentKernelTime{}; + + // Sampling + uint64_t sampleCount{}; + double percentCumulative{}; + double varianceCumulative{}; + double sigma4Cumulative{}; + double maxPercent{}; + uint32_t samplesAboveThreshold{}; +}; + +// Process Categories +std::set c_user = { + L"chrome.exe", + L"OUTLOOK.exe", + L"EXCEL.exe", + L"explorer.exe", + L"WINWORD.exe", + L"POWERPOINT.exe", + L"OfficeClickToRun.exe", + L"Microsoft.SharePoint.exe", + L"msedge.exe", + L"msedgewebview2.exe", + L"ShellExperienceHost.exe", + L"StartMenuExperienceHost.exe", + L"smartscreen.exe", + L"sihost.exe", + L"SystemSettings.exe", + L"electron.exe", + L"CrmSandbox.exe", + L"ms-teams.exe", + L"TextInputHost.exe", + L"UserOOBEBroker.exe", + L"WebViewHost.exe", + L"Widgets.exe", + L"WidgetService.exe", + L"XboxGameBarWidgets.exe", + L"teams.exe" +}; +std::set c_system = { + L"System", + L"Registry", + L"Secure System", + L"audiodg.exe", + L"ctfmon.exe", + L"LogonUI.exe", + L"MpDefenderCoreService.exe", + L"MpDlpService.exe", + L"ShellHost.exe", + L"smss.exe", + L"spoolsv.exe", + L"wininit.exe", + L"lsass.exe" +}; +std::set c_developer = { + L"cmd.exe", + L"conhost.exe", + L"console.exe", + L"OpenConsole.exe", + L"powershell.exe", + L"cl.exe", + L"link.exe", + L"devenv.exe", + L"DevHome.exe", + L"DevHomeGitHubExtension.exe", + L"python.exe", + L"build.exe", + L"msbuild.exe", + L"windbg.exe", + L"windbgx.exe", + L"EngHost.exe", + L"DbgX.Shell.exe", + L"GVFS.Mount.exe", + L"GVFS.Service.exe", + L"GVFS.ServiceUI.exe", + L"vscode.exe", + L"code.exe", + L"cpptools.exe", + L"notepad.exe", + L"notepad++.exe", + L"Wex.Services.exe", + L"Taskmgr.exe", + L"wpa.exe", + L"wpr.exe", + L"CalculatorApp.exe", + L"npm.exe", + L"winget.exe", + L"chocolatey.exe", + L"pip.exe", + L"vshost.exe", + L"VSSVC.exe", + L"VBCSCompiler.exe", + L"vcpkgsrv.exe", + L"WindowsTerminal.exe", + L"WindowsPackageManagerServer.exe", + L"reSearch.exe" +}; +std::set c_vms = { + L"vmmem", + L"vmwp.exe", // actual process name for 'vmmem' + L"vmcompute.exe", + L"vmconnect.exe", + L"vmwp.exe", + L"vmms.exe" +}; +std::set c_background = { + L"services.exe", + L"svchost.exe", + L"SCNotification.exe", + L"SecurityHealthyService.exe", + L"DevHome.QuietBackgroundProcesses.Server.exe", + L"DevHome.QuietBackgroundProcesses.ElevatedServer.exe", + L"OneDrive.exe", + L"MsMpEng.exe", + L"MsSense.exe", + L"NdrSetup.exe", + L"NisSrv.exe", + L"RuntimeBroker.exe", + L"rundll32.exe", + L"SearchHost.exe", + L"SenseCE.exe", + L"SenseNdr.exe", + L"SenseNdrX.exe", + L"SenseTVM.exe", + L"SearchIndexer.exe", + L"taskhostw.exe", + L"winlogon.exe", +}; + +ProcessCategory GetCategory(DWORD pid, std::wstring_view processName) +{ + auto search = [&](std::wstring_view processName, const auto& list) + { + auto it = std::find_if(list.begin(), list.end(), [&](const auto& elem) + { + return wil::compare_string_ordinal(processName, elem, true) == 0; + }); + auto found = (it != list.end()); + return found; + }; + + if (pid == 4) + { + // PID 4 is the System process + return ProcessCategory::System; + } + if (search(processName.data(), c_user)) + { + return ProcessCategory::User; + } + if (search(processName.data(), c_system)) + { + return ProcessCategory::System; + } + if (search(processName.data(), c_developer)) + { + return ProcessCategory::Developer; + } + if (search(processName.data(), c_vms)) + { + return ProcessCategory::Developer; + } + if (search(processName.data(), c_background)) + { + return ProcessCategory::Background; + } + return ProcessCategory::Unknown; +} + +template +void copystr(wchar_t(&dst)[N], const std::optional& src) +{ + wcscpy_s(dst, N, src.value_or(L"").substr(0, N - 1).c_str()); +} + +template +wil::unique_cotaskmem_array_ptr make_unique_cotaskmem_array_ptr(size_t numOfElements) +{ + wil::unique_cotaskmem_array_ptr result; + T* ptr = reinterpret_cast(CoTaskMemAlloc(sizeof(T) * numOfElements)); + THROW_IF_NULL_ALLOC(ptr); + *result.addressof() = ptr; + *result.size_address() = numOfElements; + return result; +} + +std::chrono::file_clock::time_point FileTimeToTimePoint(const FILETIME& fileTime) +{ + ULARGE_INTEGER uli; + uli.LowPart = fileTime.dwLowDateTime; + uli.HighPart = fileTime.dwHighDateTime; + std::chrono::file_clock::duration d{ (static_cast(fileTime.dwHighDateTime) << 32) | fileTime.dwLowDateTime }; + std::chrono::file_clock::time_point tp{ d }; + return tp; +} + +std::string FiletimeToString(const FILETIME& ft) +{ + std::chrono::file_clock::duration d{ (static_cast(ft.dwHighDateTime) << 32) | ft.dwLowDateTime }; + std::chrono::file_clock::time_point tp{ d }; + return std::format("{:%Y-%m-%d %H:%M}\n", tp); +} + +std::chrono::microseconds CpuTimeDuration(FILETIME previous, FILETIME current) +{ + if (CompareFileTime(&previous, ¤t) >= 0) + { + return std::chrono::microseconds(0); + } + + auto filetimeDeltaIn100ns = FileTimeToTimePoint(current) - FileTimeToTimePoint(previous); + auto durationMicroseconds = std::chrono::duration_cast(filetimeDeltaIn100ns); + return durationMicroseconds; +} + +int GetVirtualNumCpus() +{ + SYSTEM_INFO sysInfo; + GetSystemInfo(&sysInfo); + return sysInfo.dwNumberOfProcessors; +} + +template +std::span GetPids(DWORD (&pidArray)[size]) +{ + DWORD needed; + THROW_IF_WIN32_BOOL_FALSE(EnumProcesses(pidArray, sizeof(pidArray), &needed)); + return { &pidArray[0], needed / sizeof(DWORD) }; +} + +std::optional TryGetPackageFullNameFromTokenHelper(HANDLE token) +{ + wchar_t packageFullName[PACKAGE_FULL_NAME_MAX_LENGTH + 1]{}; + uint32_t packageFullNameLength = ARRAYSIZE(packageFullName); + if (GetPackageFullNameFromToken(token, &packageFullNameLength, packageFullName)) + { + return std::nullopt; + } + return std::wstring { packageFullName }; +} + +std::optional TryGetAppUserModelIdFromTokenHelper(HANDLE token) +{ + wchar_t aumid[APPLICATION_USER_MODEL_ID_MAX_LENGTH]{}; + uint32_t aumidLength = ARRAYSIZE(aumid); + if (GetApplicationUserModelIdFromToken(token, &aumidLength, aumid) != ERROR_SUCCESS) + { + return std::nullopt; + } + return std::wstring { aumid }; +} + +std::optional TryGetProcessName(HANDLE processHandle) +{ + static wchar_t s_buffer[MAX_PATH * 2]; + if (GetModuleFileNameExW(processHandle, nullptr, s_buffer, _countof(s_buffer)) > 0) + { + return s_buffer; + } + return std::nullopt; +} + +ProcessPerformanceInfo MakeProcessPerformanceInfo(DWORD processId) +{ + auto process = wil::unique_process_handle{ OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId) }; + if (!process) + { + // We can't open csrss.exe, so we'll just skip processes we can't open + auto info = ProcessPerformanceInfo{}; + info.process = nullptr; + info.pid = processId; + } + + auto processPathString = TryGetProcessName(process.get()); + + auto path = std::filesystem::path(processPathString.value_or(L"")); + + FILETIME createTime, exitTime, kernelTime, userTime; + THROW_IF_WIN32_BOOL_FALSE(GetProcessTimes(process.get(), &createTime, &exitTime, &kernelTime, &userTime)); + + std::optional packageFullName; + std::optional aumid; + wil::unique_handle processToken; + if (OpenProcessToken(process.get(), TOKEN_QUERY, &processToken)) + { + packageFullName = TryGetPackageFullNameFromTokenHelper(processToken.get()); + aumid = TryGetAppUserModelIdFromTokenHelper(processToken.get()); + } + + auto info = ProcessPerformanceInfo{}; + info.process = std::move(process); + info.pid = processId; + info.name = path.filename().wstring(); + info.path = path.parent_path().wstring(); + info.packageFullName = packageFullName; + info.aumid = aumid; + info.category = GetCategory(info.pid, info.name); + info.createTime = createTime; + + // Start times + info.startUserTime = userTime; + info.startKernelTime = kernelTime; + + info.previousUserTime = userTime; + info.currentUserTime = userTime; + info.previousKernelTime = kernelTime; + info.currentKernelTime = kernelTime; + + return info; +} + +bool UpdateProcessPerformanceInfo(ProcessPerformanceInfo& info) +{ + FILETIME createTime, exitTime, kernelTime, userTime; + THROW_IF_WIN32_BOOL_FALSE(GetProcessTimes(info.process.get(), &createTime, &exitTime, &kernelTime, &userTime)); + + if (exitTime.dwHighDateTime != 0 || exitTime.dwLowDateTime != 0) + { + info.exitTime = info.exitTime; + return false; + } + + info.previousUserTime = info.currentUserTime; + info.currentUserTime = userTime; + info.previousKernelTime = info.currentKernelTime; + info.currentKernelTime = kernelTime; + return true; +} + +struct cancellation_mechanism +{ + std::atomic m_cancelled{}; + std::mutex m_mutex; + std::condition_variable m_cancelCondition; + + void cancel() + { + auto lock = std::scoped_lock(m_mutex); + m_cancelled = true; + m_cancelCondition.notify_all(); + } + + bool wait_for_cancel(std::chrono::milliseconds duration) + { + auto lock = std::unique_lock(m_mutex); + auto cancelHappened = m_cancelCondition.wait_for(lock, duration, [this] { + return m_cancelled.load(); + }); + return cancelHappened; + } +}; + +struct MonitorThread +{ + cancellation_mechanism m_cancellationMechanism; + std::thread m_thread; + std::mutex m_dataMutex; + + // Tracking all our process infos + std::map m_runningProcesses; + std::vector m_terminatedProcesses; + + MonitorThread(std::chrono::milliseconds periodMs) + { + if (periodMs.count() <= 0) + { + THROW_HR(E_INVALIDARG); + } + + m_thread = std::thread([this, periodMs]() { + try + { + auto numCpus = GetVirtualNumCpus(); + + while (true) + { + if (m_cancellationMechanism.m_cancelled) + { + break; + } + + std::chrono::microseconds totalMicroseconds{}; + + // Check for new processes to track + DWORD pidArray[2048]; + auto pids = GetPids(pidArray); + + auto lock = std::scoped_lock(m_dataMutex); + for (auto& pid : pids) + { + // Ignore process "0" - the 'SYSTEM 'System' process + if (pid == 0) + { + continue; + } + + // Make a new entry + if (!m_runningProcesses.contains(pid)) + { + try + { + m_runningProcesses[pid] = MakeProcessPerformanceInfo(pid); + } + CATCH_LOG(); + } + } + + // Update counts for each tracked process + for (auto it = m_runningProcesses.begin(); it != m_runningProcesses.end(); ) + { + auto pid = it->first; + + // Get entry + auto& info = it->second; + + if (!info.process) + { + // The process couldn't be opened, so we'll skip this entry + ++it; + continue; + } + + // Update entry + try + { + if (!UpdateProcessPerformanceInfo(info)) + { + // The process terminated + + // Destroy the process handle + info.process.reset(); + + // Move from the map to the terminated list + m_terminatedProcesses.push_back(std::move(info)); + it = m_runningProcesses.erase(it); + continue; + } + } + catch (...) + { + ++it; + continue; + } + + // Collect cpuTime for process + auto cpuTime = CpuTimeDuration(info.previousUserTime, info.currentUserTime); + cpuTime += CpuTimeDuration(info.previousKernelTime, info.currentKernelTime); + + double percent = (double)cpuTime.count() / std::chrono::duration_cast(periodMs).count() / (double)numCpus * 100.0f; + double variance = (double)std::pow(percent, 2.0f); + double sigma4 = (double)std::pow(percent, 4.0f); + + info.sampleCount++; + info.percentCumulative += percent; + info.varianceCumulative += variance; + info.sigma4Cumulative += sigma4; + if (percent > info.maxPercent) + { + info.maxPercent = percent; + } + if (percent > CPU_TIME_ABOVE_THRESHOLD_STRIKE_VALUE) + { + info.samplesAboveThreshold++; + } + + totalMicroseconds += cpuTime; + + ++it; + } + + // Wait for interval period or user cancellation + if (m_cancellationMechanism.wait_for_cancel(periodMs)) + { + // User cancelled + break; + } + } + } + CATCH_LOG(); + }); + } + + void Cancel() + { + m_cancellationMechanism.cancel(); + if (m_thread.joinable()) + { + m_thread.join(); + } + } + + std::vector GetProcessPerformanceSummaries() + { + auto lock = std::scoped_lock(m_dataMutex); + + std::vector summaries; + auto MakeSummary = [](const ProcessPerformanceInfo& info) + { + auto summary = ProcessPerformanceSummary{}; + auto totalUserTime = CpuTimeDuration(info.startUserTime, info.currentUserTime); + auto totalKernelTime = CpuTimeDuration(info.startKernelTime, info.currentKernelTime); + + // Process info + summary.pid = info.pid; + if (summary.pid == 4) + { + copystr(summary.name, L"[System]"); + } + else if (info.name.empty()) + { + copystr(summary.name, L"[unk]"); + } + else + { + copystr(summary.name, info.name); + } + copystr(summary.packageFullName, info.packageFullName); + copystr(summary.aumid, info.aumid); + copystr(summary.path, info.path); + summary.category = static_cast(info.category); + summary.createTime = info.createTime; + summary.exitTime = info.exitTime; + + // Sampling + summary.sampleCount = info.sampleCount; + summary.percentCumulative = info.percentCumulative; + summary.varianceCumulative = info.varianceCumulative; + summary.sigma4Cumulative = info.sigma4Cumulative; + summary.maxPercent = info.maxPercent; + summary.samplesAboveThreshold = info.samplesAboveThreshold; + + // Other + summary.totalCpuTimeInMicroseconds = totalUserTime.count() + totalKernelTime.count(); + + if (summary.sampleCount <= 0) + { + summary.sampleCount = 0; + } + return summary; + }; + + // Add summaries for running processes + for (auto const& [key, info] : m_runningProcesses) + { + summaries.push_back(MakeSummary(info)); + } + + // Add summaries for terminated processes + for (auto const& info : m_terminatedProcesses) + { + summaries.push_back(MakeSummary(info)); + } + return summaries; + } +}; + +// +// Exports +// + +extern "C" __declspec(dllexport) HRESULT StartMonitoringProcessUtilization(uint32_t periodInMs, void** context) noexcept +try +{ + auto periodMs = std::chrono::milliseconds(periodInMs); + auto monitorThread = std::make_unique(periodMs); + *context = static_cast(monitorThread.release()); + return S_OK; +} +CATCH_RETURN() + +extern "C" __declspec(dllexport) HRESULT StopMonitoringProcessUtilization(void* context) noexcept +try +{ + auto monitorThread = reinterpret_cast(context); + monitorThread->Cancel(); + return S_OK; +} +CATCH_RETURN() + +extern "C" __declspec(dllexport) HRESULT DeleteMonitoringProcessUtilization(void* context) noexcept +try +{ + if (!context) + { + return S_OK; + } + auto monitorThread = std::unique_ptr(reinterpret_cast(context)); + monitorThread->Cancel(); + monitorThread.reset(); + return S_OK; +} +CATCH_RETURN() + +extern "C" __declspec(dllexport) HRESULT GetMonitoringProcessUtilization(void* context, ProcessPerformanceSummary** ppSummaries, size_t* summaryCount) noexcept +try +{ + auto monitorThread = reinterpret_cast(context); + auto summaries = monitorThread->GetProcessPerformanceSummaries(); + + // Alloc summaries block + auto ptrSummaries = make_unique_cotaskmem_array_ptr(summaries.size()); + auto i = 0; + for (auto const& summary : summaries) + { + auto& dst = ptrSummaries.get()[i++]; + dst = summary; + } + + *summaryCount = ptrSummaries.size(); + *ppSummaries = ptrSummaries.release(); + + return S_OK; +} +CATCH_RETURN() diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.h new file mode 100644 index 0000000000..82a73c612b --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.h @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +struct ProcessPerformanceSummary +{ + // Process info + ULONG pid{}; + wchar_t name[64]; + wchar_t packageFullName[PACKAGE_FULL_NAME_MAX_LENGTH + 1]{}; + wchar_t aumid[APPLICATION_USER_MODEL_ID_MAX_LENGTH]{}; + wchar_t path[MAX_PATH * 2]{}; + uint32_t category{}; + FILETIME createTime{}; + FILETIME exitTime{}; + + // Sampling + uint64_t sampleCount{}; + double percentCumulative{}; + double varianceCumulative{}; + double sigma4Cumulative{}; + double maxPercent{}; + uint32_t samplesAboveThreshold{}; + + // Other + uint64_t totalCpuTimeInMicroseconds{}; +}; + +extern "C" __declspec(dllexport) HRESULT StartMonitoringProcessUtilization(uint32_t periodInMs, void** context) noexcept; +extern "C" __declspec(dllexport) HRESULT StopMonitoringProcessUtilization(void* context) noexcept; +extern "C" __declspec(dllexport) HRESULT GetMonitoringProcessUtilization(void* context, ProcessPerformanceSummary** ppSummaries, size_t* summaryCount) noexcept; +extern "C" __declspec(dllexport) HRESULT DeleteMonitoringProcessUtilization(void* context) noexcept; + +using unique_process_utilization_monitoring_thread = wil::unique_any; diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/dllmain.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/dllmain.cpp new file mode 100644 index 0000000000..b503a9ba44 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/dllmain.cpp @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" + +BOOL APIENTRY DllMain(HMODULE /* hModule */, DWORD ul_reason_for_call, LPVOID /* lpReserved */) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/packages.config b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/packages.config new file mode 100644 index 0000000000..09be25d9e4 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.cpp new file mode 100644 index 0000000000..40e691ba78 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.h new file mode 100644 index 0000000000..f4b4e69378 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#include diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/QuietBackgroundProcessesSessionManager.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/QuietBackgroundProcessesSessionManager.cpp index 8f5a76b7cd..3d74ecd239 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/QuietBackgroundProcessesSessionManager.cpp +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/QuietBackgroundProcessesSessionManager.cpp @@ -3,6 +3,7 @@ #include +#include #include #include @@ -11,6 +12,8 @@ #include #include +#include + #include #include "DevHome.QuietBackgroundProcesses.h" @@ -80,6 +83,24 @@ namespace ABI::DevHome::QuietBackgroundProcesses } CATCH_RETURN() + STDMETHODIMP TryGetLastPerformanceRecording(_COM_Outptr_ ABI::DevHome::QuietBackgroundProcesses::IProcessPerformanceTable** result) noexcept override + try + { + auto factory = wil::GetActivationFactory(RuntimeClass_DevHome_QuietBackgroundProcesses_PerformanceRecorderEngine); + THROW_IF_FAILED(factory->TryGetLastPerformanceRecording(result)); + + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP HasLastPerformanceRecording(boolean* result) noexcept override + try + { + *result = std::filesystem::exists(GetTemporaryPerformanceDataPath()); + return S_OK; + } + CATCH_RETURN() + private: std::mutex m_mutex; wil::com_ptr m_sessionReference; diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/DevHome.QuietBackgroundProcesses.UI.csproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/DevHome.QuietBackgroundProcesses.UI.csproj new file mode 100644 index 0000000000..9d948baf63 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/DevHome.QuietBackgroundProcesses.UI.csproj @@ -0,0 +1,38 @@ + + + + + DevHome.QuietBackgroundProcesses.UI + x86;x64;arm64 + win-x86;win-x64;win-arm64 + true + enable + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + + + + $(DefaultXamlRuntime) + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ProcessData.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ProcessData.cs new file mode 100644 index 0000000000..697a2dfeec --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ProcessData.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace DevHome.QuietBackgroundProcesses.UI; + +public class ProcessData +{ + public enum ProcessCategory + { + Unknown, + User, + System, + Developer, + Background, + } + + public ProcessData() + { + Name = string.Empty; + PackageFullName = string.Empty; + Aumid = string.Empty; + Path = string.Empty; + } + + public long Pid { get; set; } + + public string Name { get; set; } + + public string PackageFullName { get; set; } + + public string Aumid { get; set; } + + public string Path { get; set; } + + public ProcessCategory Category { get; set; } + + public DateTimeOffset CreateTime { get; set; } + + public DateTimeOffset ExitTime { get; set; } + + public ulong Samples { get; set; } + + public double Percent { get; set; } + + public double StandardDeviation { get; set; } + + public double Sigma4Deviation { get; set; } + + public double MaxPercent { get; set; } + + public TimeSpan TimeAboveThreshold { get; set; } + + public double TimeAboveThresholdInMinutes { get; set; } + + public ulong TotalCpuTimeInMicroseconds { get; set; } +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/QuietBackgroundProcessesEvent.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/QuietBackgroundProcessesEvent.cs new file mode 100644 index 0000000000..6a2507b760 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/QuietBackgroundProcessesEvent.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Tracing; +using DevHome.Telemetry; +using Microsoft.Diagnostics.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; +using Windows.Foundation.Diagnostics; + +namespace DevHome.QuietBackgroundProcesses.UI; + +[EventData] +public class QuietBackgroundProcessesEvent : EventBase +{ + public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage; + + public LoggingOpcode Opcode { get; } + + public QuietBackgroundProcessesEvent(LoggingOpcode opcode = LoggingOpcode.Info) + { + Opcode = opcode; + } + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + } +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw new file mode 100644 index 0000000000..1a8948425b --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Quiet background processes + The name of the Quiet background processes page + + + Start session + Button that starts a quiet background session + + + End session + Button that stops a quiet background session + + + Last session length: + Length of the previously run quiet session + + + Enhance resource management while developing for a maximum of 2 hours + Description of the Quiet background processes feature + + + Quiet background processes + Inline title of the Quiet background processes feature + + + An analytic summary will be available after a session has completed. The summary will include an overview of process resource consumption. The analytic summary is not available while feature is active. + A description of the Quiet background processes time window + + + Link to docs + Link that launches documentation + + + Provide feedback + Link that launches feedback + + + Related links + Label for the doc links + + + Analytic summary + Button to open the analytic summary dialog + + + + + Analytic Summary + Title of the analytic summary page + + + Analysis of process CPU utilization while the session was in use. Calculated amount of time that the process used CPU above the designated threshold. + Desciption of the analytic summary page + + + Sort by + Label for the combo box for sorting + + + Process + Sortby dropdown option + + + Type + Sortby dropdown option + + + CPU above threshold + Sortby dropdown option + + + Search for a process + Label for analytic summary save report button + + + Save report + Label for analytic summary save report button + + + Close + Label for analytic summary close button + + + + + Process + Table heading for Process column + + + Type + Table heading for Type column + + + CPU above threshold + Table heading for 'CPU time above threshold' column + + + min + Abbreviation for the word minutes + + + + + Feature not supported on this version of Windows + Indicates that this OS isn't new enough to support the feature + + + Session Error + Something went wrong when running the session + + + Session ended + The quiet session was cancelled or the time expired + + + Unable to cancel session + Something went wrong when cancelling the session + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/AnalyticSummaryPopupViewModel.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/AnalyticSummaryPopupViewModel.cs new file mode 100644 index 0000000000..0d19ddac0a --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/AnalyticSummaryPopupViewModel.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.WinUI.Collections; +using DevHome.Telemetry; +using Microsoft.UI.Xaml.Controls; +using Serilog; + +namespace DevHome.QuietBackgroundProcesses.UI.ViewModels; + +public partial class AnalyticSummaryPopupViewModel : ObservableObject +{ + // Enum for process category + public enum ProcessTableColumn + { + Process, + Type, + CPUAboveThreshold, + } + + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(AnalyticSummaryPopupViewModel)); + private readonly List _processDatas = new(); + + public int SortComboBoxIndex { get; set; } + + public AdvancedCollectionView ProcessDatasAd { get; private set; } + + private ProcessData.ProcessCategory ConvertProcessType(DevHome.QuietBackgroundProcesses.ProcessCategory inputType) + { + return (ProcessData.ProcessCategory)inputType; + } + + public AnalyticSummaryPopupViewModel(QuietBackgroundProcesses.ProcessPerformanceTable? performanceTable) + { + TelemetryFactory.Get().Log("QuietBackgroundProcesses_AnalyticSummary_Open", LogLevel.Info, new QuietBackgroundProcessesEvent()); + + try + { + if (performanceTable != null) + { + var rows = performanceTable.Rows; + foreach (var row in rows) + { + if (row != null) + { + var sampleCount = row.SampleCount; + var sampleDuration = 1; + + var entry = new ProcessData + { + Pid = row.Pid, + Name = row.Name, + PackageFullName = row.PackageFullName, + Aumid = row.Aumid, + Path = row.Path, + Category = ConvertProcessType(row.Category), + CreateTime = row.CreateTime, + ExitTime = row.ExitTime, + Samples = row.SampleCount, + Percent = row.PercentCumulative / sampleCount, + StandardDeviation = (float)Math.Sqrt(row.VarianceCumulative / sampleCount), + Sigma4Deviation = (float)Math.Sqrt(Math.Sqrt(row.Sigma4Cumulative / sampleCount)), + MaxPercent = row.MaxPercent, + TimeAboveThreshold = TimeSpan.FromSeconds(row.SamplesAboveThreshold * sampleDuration), + TotalCpuTimeInMicroseconds = row.TotalCpuTimeInMicroseconds, + }; + + entry.TimeAboveThresholdInMinutes = entry.TimeAboveThreshold.TotalMinutes; + _processDatas.Add(entry); + } + } + } + } + catch (Exception ex) + { + _log.Error("Error populating performance summary table", ex); + } + + ProcessDatasAd = new AdvancedCollectionView(_processDatas, true); + ProcessDatasAd.SortDescriptions.Add(new SortDescription("Pid", SortDirection.Descending)); + } + + private ProcessTableColumn GetProcessTableColumnFromString(string value) + { + if (string.Equals(value, "Process", StringComparison.Ordinal)) + { + return ProcessTableColumn.Process; + } + else if (string.Equals(value, "Type", StringComparison.Ordinal)) + { + return ProcessTableColumn.Type; + } + else if (string.Equals(value, "CPUAboveThreshold", StringComparison.Ordinal)) + { + return ProcessTableColumn.CPUAboveThreshold; + } + + throw new ArgumentException("Invalid value for ProcessTableColumn"); + } + + [RelayCommand] + public void FilterProcessesTextInputChanged(string filterExpression) + { + ProcessDatasAd.Filter = item => + { + try + { + if (item is DevHome.QuietBackgroundProcesses.UI.ProcessData process) + { + return + process.Name.Contains(filterExpression, StringComparison.OrdinalIgnoreCase) + || process.Category.ToString().Contains(filterExpression, StringComparison.OrdinalIgnoreCase) + || process.TimeAboveThreshold.Minutes.ToString(CultureInfo.InvariantCulture).Contains(filterExpression, StringComparison.OrdinalIgnoreCase); + } + + return false; + } + catch (Exception ex) + { + _log.Error("Filtering failed", ex); + } + + return true; + }; + + ProcessDatasAd.RefreshFilter(); + } + + [RelayCommand] + public void SortProcessesComboBoxChanged(string selectedValueString) + { + ProcessDatasAd.SortDescriptions.Clear(); + + var selectedValue = GetProcessTableColumnFromString(selectedValueString); + switch (selectedValue) + { + case ProcessTableColumn.Process: + ProcessDatasAd.SortDescriptions.Add(new SortDescription("Name", SortDirection.Ascending)); + break; + case ProcessTableColumn.Type: + ProcessDatasAd.SortDescriptions.Add(new SortDescription("Category", SortDirection.Descending)); + break; + case ProcessTableColumn.CPUAboveThreshold: + ProcessDatasAd.SortDescriptions.Add(new SortDescription("TimeAboveThreshold", SortDirection.Descending)); + break; + } + } + + public void SaveReport(string filePath) + { + // Save the report to a .csv + using (StreamWriter writer = new StreamWriter(filePath)) + { + // Write the .csv header + writer.WriteLine("Pid," + + "Name," + + "Samples," + + "Percent," + + "StandardDeviation," + + "Sigma4Deviation," + + "MaxPercent," + + "TimeAboveThreshold," + + "TotalCpuTimeInMicroseconds," + + "PackageFullName," + + "Aumid," + + "Path," + + "Category," + + "CreateTime," + + "ExitTime"); + + // Write each item from the list to the file + foreach (var data in this._processDatas) + { + var row = $"{data.Pid}," + + $"{data.Name}," + + $"{data.Samples}," + + $"{data.Percent}," + + $"{data.StandardDeviation}," + + $"{data.Sigma4Deviation}," + + $"{data.MaxPercent}," + + $"{data.TimeAboveThreshold}," + + $"{data.TotalCpuTimeInMicroseconds}," + + $"{data.PackageFullName}," + + $"{data.Aumid}," + + $"{data.Path}," + + $"{data.Category}," + + $"{data.CreateTime}," + + $"{data.ExitTime}"; + writer.WriteLine(row); + } + } + } +} diff --git a/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs similarity index 66% rename from tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs rename to tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs index 9df8d6d7a1..ff127e1c4a 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs @@ -2,17 +2,19 @@ // Licensed under the MIT License. using System; +using System.Globalization; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.WinUI; using DevHome.Common.Services; -using DevHome.QuietBackgroundProcesses; +using DevHome.Telemetry; using Microsoft.UI.Xaml; using Serilog; +using Windows.Foundation.Diagnostics; using WinUIEx; -namespace DevHome.Customization.ViewModels; +namespace DevHome.QuietBackgroundProcesses.UI.ViewModels; public partial class QuietBackgroundProcessesViewModel : ObservableObject { @@ -22,16 +24,17 @@ public partial class QuietBackgroundProcessesViewModel : ObservableObject private readonly TimeSpan _zero = new(0, 0, 0); private readonly TimeSpan _oneSecond = new(0, 0, 1); - private readonly WindowEx _windowEx; - -#nullable enable + private TimeSpan _sessionDuration; private QuietBackgroundProcessesSession? _session; -#nullable disable + private ProcessPerformanceTable? _table; [ObservableProperty] private bool _isFeaturePresent; + [ObservableProperty] + private bool _isAnalyticSummaryAvailable; + [ObservableProperty] private string _sessionStateText; @@ -39,7 +42,7 @@ public partial class QuietBackgroundProcessesViewModel : ObservableObject private bool _quietButtonChecked; [ObservableProperty] - private string _quietButtonText; + private string? _quietButtonText; private QuietBackgroundProcessesSession GetSession() { @@ -53,7 +56,7 @@ private QuietBackgroundProcessesSession GetSession() private string GetString(string id) { - var stringResource = new StringResource("DevHome.Customization.pri", "DevHome.Customization/Resources"); + var stringResource = new StringResource("DevHome.QuietBackgroundProcesses.UI.pri", "DevHome.QuietBackgroundProcesses.UI/Resources"); return stringResource.GetLocalized(id); } @@ -70,6 +73,8 @@ public QuietBackgroundProcessesViewModel( { _experimentationService = experimentationService; _windowEx = windowEx; + _sessionStateText = string.Empty; + _dispatcherTimer = new DispatcherTimer(); } public async Task LoadViewModelContentAsync() @@ -81,7 +86,30 @@ await Task.Run(async () => return; } - var isFeaturePresent = QuietBackgroundProcessesSessionManager.IsFeaturePresent(); + var isFeaturePresent = false; + try + { + isFeaturePresent = QuietBackgroundProcessesSessionManager.IsFeaturePresent(); + } + catch (Exception ex) + { + _log.Error(ex, "COM error"); + } + + var isAvailable = false; + isAvailable = _table != null; + if (!isAvailable) + { + try + { + isAvailable = QuietBackgroundProcessesSessionManager.HasLastPerformanceRecording(); + } + catch (Exception ex) + { + _log.Error(ex, "COM error"); + } + } + var running = false; long? timeLeftInSeconds = null; if (isFeaturePresent) @@ -99,6 +127,7 @@ await Task.Run(async () => await _windowEx.DispatcherQueue.EnqueueAsync(() => { IsFeaturePresent = isFeaturePresent; + IsAnalyticSummaryAvailable = isAvailable; if (IsFeaturePresent) { // Resume countdown if there's an existing quiet window @@ -107,6 +136,7 @@ await _windowEx.DispatcherQueue.EnqueueAsync(() => else { SessionStateText = GetStatusString("FeatureNotSupported"); + QuietButtonText = GetString("QuietBackgroundProcesses_QuietButton_Start"); } }); }); @@ -119,11 +149,16 @@ private void SetQuietSessionRunningState(bool running, long? timeLeftInSeconds = var seconds = timeLeftInSeconds ?? GetTimeRemaining(); StartCountdownTimer(seconds); QuietButtonText = GetString("QuietBackgroundProcesses_QuietButton_Stop"); + IsAnalyticSummaryAvailable = false; } else { _dispatcherTimer?.Stop(); QuietButtonText = GetString("QuietBackgroundProcesses_QuietButton_Start"); + if (!IsAnalyticSummaryAvailable) + { + IsAnalyticSummaryAvailable = QuietBackgroundProcessesSessionManager.HasLastPerformanceRecording(); + } } QuietButtonChecked = !running; @@ -136,12 +171,17 @@ public void QuietButtonClicked() { try { + TelemetryFactory.Get().Log("QuietBackgroundProcesses_Session", LogLevel.Critical, new QuietBackgroundProcessesEvent(LoggingOpcode.Start)); + // Launch the server, which then elevates itself, showing a UAC prompt var timeLeftInSeconds = GetSession().Start(); + _sessionDuration = TimeSpan.FromSeconds(timeLeftInSeconds); SetQuietSessionRunningState(true, timeLeftInSeconds); } catch (Exception ex) { + TelemetryFactory.Get().Log("QuietBackgroundProcesses_SessionStartError", LogLevel.Critical, new QuietBackgroundProcessesEvent()); + SessionStateText = GetStatusString("SessionError"); _log.Error(ex, "QuietBackgroundProcessesSession::Start failed"); } @@ -150,12 +190,17 @@ public void QuietButtonClicked() { try { - GetSession().Stop(); + TelemetryFactory.Get().Log("QuietBackgroundProcesses_Session", LogLevel.Critical, new QuietBackgroundProcessesEvent(LoggingOpcode.Stop)); + + _table = GetSession().Stop(); + IsAnalyticSummaryAvailable = _table != null; SetQuietSessionRunningState(false); - SessionStateText = GetStatusString("SessionEnded"); + SessionStateText = GetLastSessionLengthString(_sessionDuration - _secondsLeft); } catch (Exception ex) { + TelemetryFactory.Get().Log("QuietBackgroundProcesses_SessionStopError", LogLevel.Critical, new QuietBackgroundProcessesEvent()); + SessionStateText = GetStatusString("UnableToCancelSession"); _log.Error(ex, "QuietBackgroundProcessesSession::Stop failed"); } @@ -214,7 +259,7 @@ private void StartCountdownTimer(long timeLeftInSeconds) SessionStateText = _secondsLeft.ToString(); } - private void DispatcherTimer_Tick(object sender, object e) + private void DispatcherTimer_Tick(object? sender, object e) { // Subtract 1 second _secondsLeft = _secondsLeft.Subtract(_oneSecond); @@ -238,12 +283,36 @@ private void DispatcherTimer_Tick(object sender, object e) if (sessionEnded) { SetQuietSessionRunningState(false); + var lastSessionLength = _sessionDuration - _secondsLeft; _secondsLeft = _zero; - SessionStateText = GetStatusString("SessionEnded"); + SessionStateText = GetLastSessionLengthString(lastSessionLength); } else { SessionStateText = _secondsLeft.ToString(); // CultureInfo.InvariantCulture } } + + private string GetLastSessionLengthString(TimeSpan lastSessionLength) + { + return GetString("QuietBackgroundProcesses_Time_LastSessionLength") + " " + lastSessionLength.ToString("g", CultureInfo.CurrentCulture); + } + + public ProcessPerformanceTable? GetProcessPerformanceTable() + { + if (_table == null) + { + try + { + _table = QuietBackgroundProcessesSessionManager.TryGetLastPerformanceRecording(); + } + catch (Exception ex) + { + SessionStateText = GetStatusString("SessionError"); + _log.Error(ex, "QuietBackgroundProcessesSessionManager.TryGetLastPerformanceRecording failed"); + } + } + + return _table; + } } diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml new file mode 100644 index 0000000000..9195fb6766 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml.cs new file mode 100644 index 0000000000..10c0608248 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Extensions; +using DevHome.Common.Windows.FileDialog; +using DevHome.QuietBackgroundProcesses.UI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using WinUIEx; + +namespace DevHome.QuietBackgroundProcesses.UI.Views; + +public sealed partial class AnalyticSummaryPopup : ContentDialog +{ + private readonly WindowEx _mainWindow; + + public AnalyticSummaryPopupViewModel ViewModel + { + get; + } + + public AnalyticSummaryPopup(QuietBackgroundProcesses.ProcessPerformanceTable? performanceTable) + { + _mainWindow = Application.Current.GetService(); + + ViewModel = new AnalyticSummaryPopupViewModel(performanceTable); + + this.Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style; + this.DefaultButton = ContentDialogButton.Primary; + this.InitializeComponent(); + } + + private void SaveReportButtonClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + using var fileDialog = new WindowSaveFileDialog(); + fileDialog.AddFileType("CSV files", ".csv"); + + var filePath = fileDialog.Show(_mainWindow); + if (filePath != null) + { + ViewModel.SaveReport(filePath); + } + } +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml new file mode 100644 index 0000000000..14bd6c02f3 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml.cs new file mode 100644 index 0000000000..364220422c --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.ObjectModel; +using DevHome.QuietBackgroundProcesses.UI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.QuietBackgroundProcesses.UI.Views; + +public sealed partial class ProcessPerformanceTableControl : UserControl +{ + public ObservableCollection ProcessDatas { get; set; } = new ObservableCollection(); + + public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register( + nameof(ItemsSource), + typeof(object), + typeof(AnalyticSummaryPopupViewModel), + new PropertyMetadata(default)); + + public object ItemsSource + { + get => (object)GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + + public ProcessPerformanceTableControl() + { + this.InitializeComponent(); + } +} diff --git a/tools/Customization/DevHome.Customization/Views/QuietBackgroundProcessesView.xaml b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesView.xaml similarity index 72% rename from tools/Customization/DevHome.Customization/Views/QuietBackgroundProcessesView.xaml rename to tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesView.xaml index efe237d265..b92dfcbbe6 100644 --- a/tools/Customization/DevHome.Customization/Views/QuietBackgroundProcessesView.xaml +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesView.xaml @@ -1,6 +1,6 @@ - + @@ -35,7 +34,16 @@ - + + + + private readonly Func _removalAction; + /// + /// Callback action to add the newly created compute system. + /// + private readonly Action _addComputeSystemAction; + public CreateComputeSystemOperation Operation { get; } public CreateComputeSystemOperationViewModel( @@ -48,11 +55,13 @@ public CreateComputeSystemOperationViewModel( StringResource stringResource, WindowEx windowsEx, Func removalAction, + Action addComputeSystemAction, CreateComputeSystemOperation operation) { - IsCreationInProgress = true; + IsOperationInProgress = true; _windowEx = windowsEx; _removalAction = removalAction; + _addComputeSystemAction = addComputeSystemAction; _stringResource = stringResource; _computeSystemManager = computeSystemManager; Operation = operation; @@ -93,13 +102,11 @@ private void UpdateStatusIfCompleted(CreateComputeSystemResult createComputeSyst _windowEx.DispatcherQueue.TryEnqueue(() => { // Update the creation status - IsCreationInProgress = false; + IsOperationInProgress = false; if (createComputeSystemResult.Result.Status == ProviderOperationStatus.Success) { - UpdateUiMessage(_stringResource.GetLocalized("SuccessMessageForCreateComputeSystem", ProviderDisplayName)); - ComputeSystem = new(createComputeSystemResult.ComputeSystem); - State = ComputeSystemState.Created; - StateColor = CardStateColor.Success; + RemoveViewModelFromUI(); + AddComputeSystemToUI(createComputeSystemResult); } else { @@ -107,8 +114,6 @@ private void UpdateStatusIfCompleted(CreateComputeSystemResult createComputeSyst State = ComputeSystemState.Unknown; StateColor = CardStateColor.Failure; } - - RemoveEventHandlers(); }); } @@ -147,4 +152,21 @@ private void RemoveViewModelFromUI() _computeSystemManager.RemoveOperation(Operation); }); } + + private void AddComputeSystemToUI(CreateComputeSystemResult result) + { + _windowEx.DispatcherQueue.TryEnqueue(async () => + { + var newComputeSystemViewModel = new ComputeSystemViewModel( + _computeSystemManager, + result.ComputeSystem, + Operation.ProviderDetails.ComputeSystemProvider, + _removalAction, + Operation.ProviderDetails.ExtensionWrapper.PackageFullName, + _windowEx); + + await newComputeSystemViewModel.InitializeCardDataAsync(); + _addComputeSystemAction(newComputeSystemViewModel); + }); + } } diff --git a/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs index 659a8be6a2..0afbe98970 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs @@ -29,6 +29,8 @@ public partial class LandingPageViewModel : ObservableObject, IDisposable { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(LandingPageViewModel)); + private readonly AutoResetEvent _computeSystemLoadWait = new(false); + private readonly WindowEx _windowEx; private readonly EnvironmentsExtensionsService _environmentExtensionsService; @@ -43,6 +45,8 @@ public partial class LandingPageViewModel : ObservableObject, IDisposable private readonly object _lock = new(); + private bool _disposed; + private bool _wasSyncButtonClicked; public bool IsLoading { get; set; } @@ -238,11 +242,11 @@ private void SetupCreateComputeSystemOperationForUI() var curOperations = _computeSystemManager.GetRunningOperationsForCreation(); for (var i = ComputeSystemCards.Count - 1; i >= 0; i--) { - if (ComputeSystemCards[i].IsCreateComputeSystemOperation) + if (ComputeSystemCards[i].IsCreateComputeSystemOperation) { var operationViewModel = ComputeSystemCards[i] as CreateComputeSystemOperationViewModel; operationViewModel!.RemoveEventHandlers(); - ComputeSystemCards.RemoveAt(i); + ComputeSystemCards.RemoveAt(i); } } @@ -250,7 +254,7 @@ private void SetupCreateComputeSystemOperationForUI() foreach (var operation in curOperations) { // this is a new operation so we need to create a view model for it. - ComputeSystemCards.Add(new CreateComputeSystemOperationViewModel(_computeSystemManager, _stringResource, _windowEx, ComputeSystemCards.Remove, operation)); + ComputeSystemCards.Add(new CreateComputeSystemOperationViewModel(_computeSystemManager, _stringResource, _windowEx, ComputeSystemCards.Remove, AddNewlyCreatedComputeSystem, operation)); _log.Information($"Found new create compute system operation for provider {operation.ProviderDetails.ComputeSystemProvider}, with name {operation.EnvironmentName}"); } } @@ -292,6 +296,7 @@ await _windowEx.DispatcherQueue.EnqueueAsync(async () => _computeSystemManager, computeSystemList.ElementAt(i), provider, + ComputeSystemCards.Remove, packageFullName, _windowEx); await computeSystemViewModel.InitializeCardDataAsync(); @@ -303,6 +308,8 @@ await _windowEx.DispatcherQueue.EnqueueAsync(async () => _log.Error(ex, $"Exception occurred while adding Compute systems to environments page for provider: {provider.Id}"); } }); + + _computeSystemLoadWait.Set(); } /// @@ -389,8 +396,47 @@ public void SortHandler() } } + private void AddNewlyCreatedComputeSystem(ComputeSystemViewModel computeSystemViewModel) + { + Task.Run(() => + { + if (IsLoading) + { + _computeSystemLoadWait.WaitOne(); + } + + lock (_lock) + { + var viewModel = ComputeSystemCards.FirstOrDefault(viewBase => viewBase.ComputeSystemId.Equals(computeSystemViewModel.ComputeSystemId, StringComparison.OrdinalIgnoreCase)); + + if (viewModel == null) + { + _windowEx.DispatcherQueue.EnqueueAsync(() => + { + ComputeSystemCards.Add(computeSystemViewModel); + }); + } + } + }); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _computeSystemLoadWait.Dispose(); + } + + _disposed = true; + } + } + public void Dispose() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); GC.SuppressFinalize(this); } } diff --git a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml index eed8ec041b..e53313dc5c 100644 --- a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml +++ b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml @@ -36,10 +36,11 @@ + Visibility="{x:Bind ShouldShowLaunchOperation, Mode=OneWay}" + VerticalAlignment="Top" + Command="{x:Bind LaunchActionCommand}" + x:Uid="ms-resource:///DevHome.Environments/Resources/LaunchButton" + Style="{StaticResource CardBodySplitButtonStyle}"> @@ -147,7 +148,9 @@ StateColor="{x:Bind StateColor, Mode=OneWay}" ComputeSystemProperties="{x:Bind Properties, Mode=OneWay}" ComputeSystemPropertyTemplate="{StaticResource BottomRowProperties}" - ActionControlTemplate="{StaticResource LaunchButton}"/> + ActionControlTemplate="{StaticResource LaunchButton}" + ComputeSystemOperationStatus="{x:Bind UiMessageToDisplay, Mode=OneWay}" + ShouldShowInDefiniteProgress="{x:Bind IsOperationInProgress, Mode=OneWay}"/> @@ -170,8 +173,8 @@ + ComputeSystemOperationStatus="{x:Bind UiMessageToDisplay, Mode=OneWay}" + ShouldShowInDefiniteProgress="{x:Bind IsOperationInProgress, Mode=OneWay}"/> diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs index 93469e207a..819ea2dfaf 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs @@ -81,6 +81,8 @@ public Guid ActivityId public bool IsSettingUpLocalMachine => CurrentSetupFlowKind == SetupFlowKind.LocalMachine; + public bool IsInCreateEnvironmentFlow => CurrentSetupFlowKind == SetupFlowKind.CreateEnvironment; + /// /// Occurs right before a page changes /// @@ -193,12 +195,16 @@ public void ReleaseRemoteOperationObject() partial void OnCurrentPageViewModelChanging(SetupPageViewModelBase value) => PageChanging?.Invoke(null, EventArgs.Empty); + public bool IsNavigatingForward { get; private set; } + + public bool IsNavigatingBackward { get; private set; } + [RelayCommand(CanExecute = nameof(CanGoToPreviousPage))] public async Task GoToPreviousPage() { // If an adaptive card is being shown in the setup flow, we need to invoke the action // of the previous button in the action set to move the flow to the previous page in the adaptive card. - if (DevHomeActionSetRenderer?.ActionButtonInvoker != null) + if (DevHomeActionSetRenderer?.ActionButtonInvoker != null && !CurrentPageViewModel.IsInitialAdaptiveCardPage) { DevHomeActionSetRenderer.InitiateAction(_adaptiveCardPreviousButtonId); } @@ -257,7 +263,7 @@ public async Task InitializeElevatedServerAsync() private async Task SetCurrentPageIndex(int index) { - var movingForward = index > _currentPageIndex; + IsNavigatingForward = index > _currentPageIndex; SetupPageViewModelBase previousPage = CurrentPageViewModel; @@ -271,12 +277,17 @@ private async Task SetCurrentPageIndex(int index) ShouldShowDoneButton = _currentPageIndex == FlowPages.Count - 1; // Do post-navigation tasks only when moving forwards, not when going back to a previous page. - if (movingForward) + if (IsNavigatingForward) { await previousPage?.OnNavigateFromAsync(); } + IsNavigatingBackward = !IsNavigatingForward; await CurrentPageViewModel?.OnNavigateToAsync(); + + // Reset navigation now that the navigation tasks are done. + IsNavigatingForward = false; + IsNavigatingBackward = false; } /// diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs index d35f213fd0..ac3e3edb2e 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs @@ -284,6 +284,7 @@ public static class StringResourceKey public static readonly string EnvironmentCreationReviewExpanderDescription = nameof(EnvironmentCreationReviewExpanderDescription); public static readonly string CreateEnvironmentButtonText = nameof(CreateEnvironmentButtonText); public static readonly string SetupShellReviewPageDescriptionForEnvironmentCreation = nameof(SetupShellReviewPageDescriptionForEnvironmentCreation); + public static readonly string EnvironmentCreationAdaptiveCardLoadingMessage = nameof(EnvironmentCreationAdaptiveCardLoadingMessage); // Summary page public static readonly string SummaryPageOpenDashboard = nameof(SummaryPageOpenDashboard); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw index 94620a5bcb..804ff744f0 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -1847,4 +1847,8 @@ We've started creating your environment Text to tell the user that an environment is being created. Environments can be local or cloud virtual machines + + Loading adaptive card from the {0} provider + Text to tell the user that Dev Home is loading the adaptive card content from a specific Dev Home extension provider. {0} is the display name of the provider + \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs index 55344a5365..3d539feb70 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs @@ -62,6 +62,9 @@ public partial class EnvironmentCreationOptionsViewModel : SetupPageViewModelBas [ObservableProperty] private string _sessionErrorMessage; + [ObservableProperty] + private string _adaptiveCardLoadingMessage; + public EnvironmentCreationOptionsViewModel( ISetupFlowStringResource stringResource, SetupFlowOrchestrator orchestrator, @@ -92,6 +95,7 @@ public EnvironmentCreationOptionsViewModel( _elementRegistration.Set(DevHomeContentDialogContent.AdaptiveElementType, new DevHomeContentDialogContentParser()); _adaptiveCardRenderingService = renderingService; Orchestrator.CurrentSetupFlowKind = SetupFlowKind.CreateEnvironment; + IsInitialAdaptiveCardPage = true; } /// @@ -124,16 +128,24 @@ protected async override Task OnFirstNavigateToAsync() /// protected async override Task OnEachNavigateToAsync() { + // Don't get a new adaptive card session if we went from review page back to + // configure environment creation page. + if (Orchestrator.IsNavigatingBackward) + { + return; + } + await Task.CompletedTask; var curSelectedProviderId = _curProviderDetails?.ComputeSystemProvider?.Id ?? string.Empty; var upcomingSelectedProviderId = _upcomingProviderDetails?.ComputeSystemProvider?.Id; - // Selected compute system provider may have changed so we need to update the adaptive card in the UI - // with new a adaptive card from the new provider. + // Selected compute system provider changed so we need to update the adaptive card in the UI + // with a new adaptive card from the new provider. _curProviderDetails = _upcomingProviderDetails; IsAdaptiveCardSessionLoaded = false; + AdaptiveCardLoadingMessage = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationAdaptiveCardLoadingMessage, _curProviderDetails.ComputeSystemProvider.DisplayName); // Its possible that an extension could take a long time to load the adaptive card session. // So we run this on a background thread to prevent the UI from freezing. @@ -223,6 +235,7 @@ private void OnRenderedAdaptiveCardAction(object sender, AdaptiveActionEventArgs _windowEx.DispatcherQueue.TryEnqueue(async () => { IsAdaptiveCardSessionLoaded = false; + AdaptiveCardLoadingMessage = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationAdaptiveCardLoadingMessage, _curProviderDetails.ComputeSystemProvider.DisplayName); // Send the inputs and actions that the user entered back to the extension. await _extensionAdaptiveCardSession.OnAction(args.Action.ToJson().Stringify(), args.Inputs.AsJson().Stringify()); @@ -301,6 +314,7 @@ private async Task GetAdaptiveCardRenderer() { var renderer = await _adaptiveCardRenderingService.GetRendererAsync(); renderer.ElementRenderers.Set(DevHomeSettingsCardChoiceSet.AdaptiveElementType, new ItemsViewChoiceSet("SettingsCardWithButtonThatLaunchesContentDialog")); + renderer.ElementRenderers.Set("Input.ChoiceSet", new DevHomeChoiceSetWithDynamicRefresh()); // We need to keep the same renderer for the ActionSet that is hooked up to the orchestrator as it will have the adaptive card // context needed to invoke the adaptive card actions from outside the adaptive card. diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs index 0eb07e27c6..3ea136a08b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs @@ -41,15 +41,17 @@ public partial class ReviewViewModel : SetupPageViewModelBase [NotifyCanExecuteChangedFor(nameof(SetUpCommand))] private bool _canSetUp; - [ObservableProperty] + [ObservableProperty] private string _reviewPageTitle; - [ObservableProperty] + [ObservableProperty] private string _reviewPageExpanderDescription; - [ObservableProperty] - private string _reviewPageDescription; - + [ObservableProperty] + private string _reviewPageDescription; + + public bool ShouldShowGenerateConfigurationFile => !Orchestrator.IsInCreateEnvironmentFlow; + public bool HasApplicationsToInstall => Orchestrator.GetTaskGroup()?.SetupTasks.Any() == true; public bool RequiresTermsAgreement => HasApplicationsToInstall; @@ -116,15 +118,15 @@ protected async override Task OnEachNavigateToAsync() SelectedReviewTab = ReviewTabs.FirstOrDefault(); // If the CreateEnvironmentTaskGroup is present, update the setup button text to "Create Environment" - // and page title to "Review your environment" - if (Orchestrator.GetTaskGroup() != null) - { - NextPageButtonText = StringResource.GetLocalized(StringResourceKey.CreateEnvironmentButtonText); + // and page title to "Review your environment" + if (Orchestrator.GetTaskGroup() != null) + { + NextPageButtonText = StringResource.GetLocalized(StringResourceKey.CreateEnvironmentButtonText); PageTitle = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewPageTitle); ReviewPageExpanderDescription = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewExpanderDescription); - ReviewPageDescription = StringResource.GetLocalized(StringResourceKey.SetupShellReviewPageDescriptionForEnvironmentCreation); - } - + ReviewPageDescription = StringResource.GetLocalized(StringResourceKey.SetupShellReviewPageDescriptionForEnvironmentCreation); + } + NextPageButtonToolTipText = HasTasksToSetUp ? null : StringResource.GetLocalized(StringResourceKey.ReviewNothingToSetUpToolTip); UpdateCanSetUp(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs index cc36f1b534..481db11725 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs @@ -97,6 +97,12 @@ public SetupFlowOrchestrator Orchestrator get; } + /// + /// Gets or sets a value indicating whether the base object is the page that displays the initial adaptive card + /// within a flow that supports adaptive cards with a next and previous button. + /// + public bool IsInitialAdaptiveCardPage { get; protected set; } + public bool IsLastStepPage => IsStepPage && Orchestrator.SetupStepPages.LastOrDefault() == this; public bool IsPastPage => Orchestrator.IsPastPage(this); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs index 24ca943ebb..523c65be4a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Threading.Tasks; using AdaptiveCards.Rendering.WinUI3; using CommunityToolkit.Mvvm.Messaging; using DevHome.SetupFlow.Models.Environments; @@ -20,6 +22,8 @@ public sealed partial class CreateEnvironmentReviewView : UserControl, IRecipien public CreateEnvironmentReviewViewModel ViewModel => (CreateEnvironmentReviewViewModel)this.DataContext; + private FrameworkElement _adaptiveCardFrameWorkElement; + public CreateEnvironmentReviewView() { this.InitializeComponent(); @@ -28,12 +32,17 @@ public CreateEnvironmentReviewView() private void ViewUnloaded(object sender, RoutedEventArgs e) { - AdaptiveCardGrid.Children.Clear(); + // View unloaded so re-add elements back to original card now that we're done + if (_adaptiveCardFrameWorkElement is Grid cardGrid) + { + AddUIElementsToGrid(AdaptiveCardGrid, cardGrid); + } + WeakReferenceMessenger.Default.UnregisterAll(this); } /// - /// Recieves the adaptive card from the view model, when the view model finishes loading it. + /// Receives the adaptive card from the view model, when the view model finishes loading it. /// public void Receive(NewAdaptiveCardAvailableMessage message) { @@ -62,14 +71,14 @@ private void AddAdaptiveCardToUI(RenderedAdaptiveCard renderedAdaptiveCard) { try { - var frameworkElement = renderedAdaptiveCard?.FrameworkElement; - if (frameworkElement == null) + _adaptiveCardFrameWorkElement = renderedAdaptiveCard?.FrameworkElement; + + // The grid may be in use by other views. Remove all the child elements + // then add them to this views page to further prevent exceptions. + if (_adaptiveCardFrameWorkElement is Grid cardGrid) { - return; + AddUIElementsToGrid(cardGrid, AdaptiveCardGrid); } - - AdaptiveCardGrid.Children.Clear(); - AdaptiveCardGrid.Children.Add(frameworkElement); } catch (Exception ex) { @@ -77,4 +86,23 @@ private void AddAdaptiveCardToUI(RenderedAdaptiveCard renderedAdaptiveCard) _log.Error(ex, "Error adding adaptive card UI in CreateEnvironmentReviewView"); } } + + private void AddUIElementsToGrid(Grid gridToRemoveItems, Grid gridToAddItems) + { + var listOfElements = new List(); + foreach (var item in gridToRemoveItems.Children) + { + listOfElements.Add(item); + } + + if (listOfElements.Count > 0) + { + gridToRemoveItems.Children.Clear(); + gridToAddItems.Children.Clear(); + foreach (var item in listOfElements) + { + gridToAddItems.Children.Add(item); + } + } + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml index fa967365f7..54879c0109 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml @@ -10,6 +10,7 @@ + @@ -30,8 +31,9 @@ - + @@ -39,23 +41,34 @@ IsOpen="True" x:Uid="ErrorRetrievingAdaptiveCardSession" Severity="Error" - Message="{x:Bind ViewModel.SessionErrorMessage}" > + Message="{x:Bind ViewModel.SessionErrorMessage, Mode=OneWay}" > - - + + x:Name="AdaptiveCardGrid" + Padding="-15"/> + + + + + Width="20" + Height="20"/> + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs index 7a438b6ae8..c965c9caec 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs @@ -2,8 +2,10 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using AdaptiveCards.Rendering.WinUI3; using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.WinUI; using DevHome.SetupFlow.Models.Environments; using DevHome.SetupFlow.ViewModels.Environments; using Microsoft.UI.Xaml; @@ -19,6 +21,8 @@ public sealed partial class EnvironmentCreationOptionsView : UserControl, IRecip public EnvironmentCreationOptionsViewModel ViewModel => (EnvironmentCreationOptionsViewModel)this.DataContext; + private FrameworkElement _adaptiveCardFrameWorkElement; + public EnvironmentCreationOptionsView() { this.InitializeComponent(); @@ -27,7 +31,12 @@ public EnvironmentCreationOptionsView() private void ViewUnloaded(object sender, RoutedEventArgs e) { - AdaptiveCardGrid.Children.Clear(); + // View unloaded so re-add elements back to original card now that we're done + if (_adaptiveCardFrameWorkElement is Grid cardGrid) + { + AddUIElementsToGrid(AdaptiveCardGrid, cardGrid); + } + WeakReferenceMessenger.Default.UnregisterAll(this); } @@ -63,10 +72,13 @@ private void AddAdaptiveCardToUI(RenderedAdaptiveCard adaptiveCardData) { try { - if (adaptiveCardData?.FrameworkElement != null) + _adaptiveCardFrameWorkElement = adaptiveCardData?.FrameworkElement; + + // The grid may be in use by other views. Remove all the child elements + // then add them to this views page to further prevent exceptions. + if (_adaptiveCardFrameWorkElement is Grid cardGrid) { - AdaptiveCardGrid.Children.Clear(); - AdaptiveCardGrid.Children.Add(adaptiveCardData.FrameworkElement); + AddUIElementsToGrid(cardGrid, AdaptiveCardGrid); } } catch (Exception ex) @@ -75,4 +87,23 @@ private void AddAdaptiveCardToUI(RenderedAdaptiveCard adaptiveCardData) _log.Error(ex, "Error adding adaptive card UI in EnvironmentCreationOptionsView"); } } + + private void AddUIElementsToGrid(Grid gridToRemoveItems, Grid gridToAddItems) + { + var listOfElements = new List(); + foreach (var item in gridToRemoveItems.Children) + { + listOfElements.Add(item); + } + + if (listOfElements.Count > 0) + { + gridToRemoveItems.Children.Clear(); + gridToAddItems.Children.Clear(); + foreach (var item in listOfElements) + { + gridToAddItems.Children.Add(item); + } + } + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml index 30efe7f7f5..634c99aeae 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml @@ -47,6 +47,7 @@ diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml index 576ae8c9f5..b270959109 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml @@ -73,7 +73,7 @@ - + -public partial class ComputeSystemViewModel : ComputeSystemCardBase +public partial class ComputeSystemViewModel : ComputeSystemCardBase, IRecipient, IRecipient { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ComputeSystemViewModel)); @@ -78,6 +82,7 @@ public ComputeSystemViewModel( _computeSystemManager.ComputeSystemStateChanged += OnComputeSystemStateChanged; _stringResource = new StringResource("DevHome.Environments.pri", "DevHome.Environments/Resources"); + RegisterForAllOperationMessages(); } public async Task InitializeCardDataAsync() @@ -143,6 +148,9 @@ public void RemoveStateChangedHandler() { ComputeSystem!.StateChanged -= _computeSystemManager.OnComputeSystemStateChanged; _computeSystemManager.ComputeSystemStateChanged -= OnComputeSystemStateChanged; + + // Unregister from all operation messages + WeakReferenceMessenger.Default.UnregisterAll(this); } [RelayCommand] @@ -216,6 +224,77 @@ private void LogFailure(ComputeSystemOperationResult? computeSystemOperationResu } } + /// + /// Implements the Receive method from the IRecipient interface. When this message + /// is received we fire the first telemetry event to capture which operation and provider is starting. + /// + /// The object that holds the data needed to capture the operationInvoked telemetry data + public void Receive(ComputeSystemOperationStartedMessage message) + { + _windowEx.DispatcherQueue.TryEnqueue(() => + { + var data = message.Value; + IsOperationInProgress = true; + + _log.Information($"operation '{data.ComputeSystemOperation}' starting for Compute System: {Name} at {DateTime.Now}"); + TelemetryFactory.Get().Log( + "Environment_OperationInvoked_Event", + LogLevel.Measure, + new EnvironmentOperationUserEvent(data.TelemetryStatus, data.ComputeSystemOperation, ComputeSystem!.AssociatedProviderId, data.AdditionalContext, data.ActivityId)); + }); + } + + /// + /// Implements the Receive method from the IRecipient interface. When this message + /// is received the operation is completed and we can log the result of the operation. + /// + /// The object that holds the data needed to capture the operationInvoked telemetry data + public void Receive(ComputeSystemOperationCompletedMessage message) + { + _windowEx.DispatcherQueue.TryEnqueue(() => + { + var data = message.Value; + _log.Information($"operation '{data.ComputeSystemOperation}' completed for Compute System: {Name} at {DateTime.Now}"); + + var completionStatus = EnvironmentsTelemetryStatus.Succeeded; + + if ((data.OperationResult == null) || (data.OperationResult.Result.Status == ProviderOperationStatus.Failure)) + { + completionStatus = EnvironmentsTelemetryStatus.Failed; + LogFailure(data.OperationResult); + } + + TelemetryFactory.Get().Log( + "Environment_OperationInvoked_Event", + LogLevel.Measure, + new EnvironmentOperationUserEvent(completionStatus, data.ComputeSystemOperation, ComputeSystem!.AssociatedProviderId, data.AdditionalContext, data.ActivityId)); + + IsOperationInProgress = false; + }); + } + + /// + /// Register the ViewModel to receive messages for the start and completion of operations from all view models within the + /// DotOperation and LaunchOperation lists. When there is an operation this ViewModel will receive the started and + /// the completed messages. + /// + private void RegisterForAllOperationMessages() + { + _log.Information($"Registering ComputeSystemViewModel '{Name}' from provider '{ProviderDisplayName}' with WeakReferenceMessenger at {DateTime.Now}"); + + foreach (var dotOperation in DotOperations!) + { + WeakReferenceMessenger.Default.Register(this, dotOperation); + WeakReferenceMessenger.Default.Register(this, dotOperation); + } + + foreach (var launchOperation in LaunchOperations!) + { + WeakReferenceMessenger.Default.Register(this, launchOperation); + WeakReferenceMessenger.Default.Register(this, launchOperation); + } + } + private bool IsComputeSystemStateTransitioning(ComputeSystemState state) { switch (state) diff --git a/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs index 32d002d684..c55516bb07 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs @@ -2,12 +2,22 @@ // Licensed under the MIT License. using System; +using System.Configuration.Provider; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.Environments.Models; using Microsoft.Windows.DevHome.SDK; +using Windows.Foundation; namespace DevHome.Environments.ViewModels; +/// +/// OperationKind represents two different types of operations +/// the view model can perform. +/// 1. ExtensionTask: An environment extension's method that returns a task +/// 2. DevHomeAction: A Action that is internal to Dev Home. E.g an Action to Remove an item from the UI. +/// public enum OperationKind { ExtensionTask, @@ -18,13 +28,17 @@ public enum OperationKind /// Represents an operation that can be performed on a compute system. /// This is used to populate the launch and dot buttons on the compute system card. /// -public partial class OperationsViewModel +public partial class OperationsViewModel : IEquatable { + private readonly Guid _operationId = Guid.NewGuid(); + private readonly OperationKind _operationKind; + private readonly string _additionalContext = string.Empty; + public string Name { get; } - public string? Description { get; set; } + public ComputeSystemOperations ComputeSystemOperation { get; } public string IconGlyph { get; } @@ -32,12 +46,28 @@ public partial class OperationsViewModel private Action? DevHomeAction { get; } - public OperationsViewModel(string name, string icon, Func> command) + public OperationsViewModel(string name, string icon, Func> command, ComputeSystemOperations computeSystemOperation) + { + _operationKind = OperationKind.ExtensionTask; + Name = name; + IconGlyph = icon; + ExtensionTask = command; + ComputeSystemOperation = computeSystemOperation; + } + + public OperationsViewModel( + string name, + string icon, + Func> command, + ComputeSystemOperations computeSystemOperation, + string additionalContext) { _operationKind = OperationKind.ExtensionTask; Name = name; IconGlyph = icon; ExtensionTask = command; + ComputeSystemOperation = computeSystemOperation; + _additionalContext = additionalContext; } public OperationsViewModel(string name, string icon, Action command) @@ -51,7 +81,6 @@ public OperationsViewModel(string name, string icon, Action command) [RelayCommand] public void InvokeAction() { - // We'll need to disable the card UI while the operation is in progress and handle failures. Task.Run(async () => { if (_operationKind == OperationKind.DevHomeAction) @@ -60,8 +89,47 @@ public void InvokeAction() return; } - // We'll need to handle the case where the DevHome service is not available. - await ExtensionTask!(string.Empty); + var activityId = Guid.NewGuid(); + WeakReferenceMessenger.Default.Send(new ComputeSystemOperationStartedMessage(new(ComputeSystemOperation, _additionalContext, activityId)), this); + + var result = await ExtensionTask!(string.Empty); + + WeakReferenceMessenger.Default.Send(new ComputeSystemOperationCompletedMessage(new(ComputeSystemOperation, result, _additionalContext, activityId)), this); }); } + + /// + /// Compares the current instance of the object with another instance of the object to check if they are the same. + /// + /// A OperationsViewModel to compare the current object with + /// True is the object in parameter is equal to the current object + public bool Equals(OperationsViewModel? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (ReferenceEquals(null, other)) + { + return false; + } + + return false; + } + + public override bool Equals(object? obj) + { + return Equals(obj as OperationsViewModel); + } + + public override int GetHashCode() + { + return $"{Name}#{_operationKind}#{IconGlyph}#{_operationId}".GetHashCode(); + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs index 261ac847ce..12fdc8e58a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs @@ -12,12 +12,15 @@ using DevHome.Common.Environments.Services; using DevHome.Common.Extensions; using DevHome.Common.Services; +using DevHome.Common.TelemetryEvents.Environments; +using DevHome.Common.TelemetryEvents.SetupFlow.Environments; using DevHome.Common.Views; using DevHome.SetupFlow.Common.Exceptions; using DevHome.SetupFlow.Exceptions; using DevHome.SetupFlow.Models.WingetConfigure; using DevHome.SetupFlow.Services; using DevHome.SetupFlow.ViewModels; +using DevHome.Telemetry; using Microsoft.UI.Xaml; using Microsoft.Windows.DevHome.SDK; using Projection::DevHome.SetupFlow.ElevatedComponent; @@ -359,11 +362,12 @@ public IAsyncOperation Execute() { try { + _log.Information($"Starting configuration on {ComputeSystemName}"); UserNumberOfAttempts = 1; AddMessage(_stringResource.GetLocalized(StringResourceKey.SetupTargetExtensionApplyingConfiguration, ComputeSystemName), MessageSeverityKind.Info); WingetConfigFileString = _configurationFileBuilder.BuildConfigFileStringFromTaskGroups(_setupFlowOrchestrator.TaskGroups, ConfigurationFileKind.SetupTarget); var computeSystem = _computeSystemManager.ComputeSystemSetupItem.ComputeSystemToSetup; - var applyConfigurationOperation = computeSystem.ApplyConfiguration(WingetConfigFileString); + var applyConfigurationOperation = computeSystem.CreateApplyConfigurationOperation(WingetConfigFileString); applyConfigurationOperation.ConfigurationSetStateChanged += OnApplyConfigurationOperationChanged; applyConfigurationOperation.ActionRequired += OnActionRequired; @@ -375,6 +379,11 @@ public IAsyncOperation Execute() var tokenSource = new CancellationTokenSource(); tokenSource.CancelAfter(TimeSpan.FromMinutes(10)); + TelemetryFactory.Get().Log( + "Environment_OperationInvoked_Event", + LogLevel.Measure, + new EnvironmentOperationUserEvent(EnvironmentsTelemetryStatus.Started, ComputeSystemOperations.ApplyConfiguration, computeSystem.AssociatedProviderId, string.Empty, _setupFlowOrchestrator.ActivityId)); + ApplyConfigurationAsyncOperation = applyConfigurationOperation.StartAsync(); var result = await ApplyConfigurationAsyncOperation.AsTask().WaitAsync(tokenSource.Token); @@ -401,11 +410,13 @@ public IAsyncOperation Execute() throw Result.ProviderResult.ExtendedError ?? throw new SDKApplyConfigurationSetResultException("Applying the configuration failed but we weren't able to check the ProviderOperation results extended error."); } + LogCompletionTelemetry(TaskFinishedState.Success); return TaskFinishedState.Success; } catch (Exception e) { _log.Error(e, $"Failed to apply configuration on target machine."); + LogCompletionTelemetry(TaskFinishedState.Failure); return TaskFinishedState.Failure; } }).AsAsyncOperation(); @@ -499,4 +510,15 @@ await _windowEx.DispatcherQueue.EnqueueAsync(async () => return (_stringResource.GetLocalized(StringResourceKey.ConfigurationUnitSummaryFull, unit.Intent, unit.Type, packageId, unitDescription), packageName); } + + private void LogCompletionTelemetry(TaskFinishedState taskFinishedState) + { + var status = taskFinishedState == TaskFinishedState.Success ? EnvironmentsTelemetryStatus.Succeeded : EnvironmentsTelemetryStatus.Failed; + var computeSystem = _computeSystemManager.ComputeSystemSetupItem.ComputeSystemToSetup; + + TelemetryFactory.Get().Log( + "Environment_OperationInvoked_Event", + LogLevel.Measure, + new EnvironmentOperationUserEvent(status, ComputeSystemOperations.ApplyConfiguration, computeSystem.AssociatedProviderId, string.Empty, _setupFlowOrchestrator.ActivityId)); + } } From 12a04c6b0ff0929f88fcc4e2f038cd65f383fe82 Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Wed, 17 Apr 2024 12:06:29 -0400 Subject: [PATCH 113/121] Move PowerToys.dsc.yaml to correct directory (#2652) --- .../DscResources/PowerToysConfigure/PowerToys.dsc.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {sampleConfigurations => docs/sampleConfigurations}/DscResources/PowerToysConfigure/PowerToys.dsc.yaml (100%) diff --git a/sampleConfigurations/DscResources/PowerToysConfigure/PowerToys.dsc.yaml b/docs/sampleConfigurations/DscResources/PowerToysConfigure/PowerToys.dsc.yaml similarity index 100% rename from sampleConfigurations/DscResources/PowerToysConfigure/PowerToys.dsc.yaml rename to docs/sampleConfigurations/DscResources/PowerToysConfigure/PowerToys.dsc.yaml From f02dde092791428d3ddc6b4c0af646e013bc9da9 Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:13:50 -0400 Subject: [PATCH 114/121] Wrap WidgetDefinition objects in a COM-safe wrapper (#2622) * delete widget event we don't care about * Use ComSafeWidgetDefinition * remove provider events we don't care about * orderby * Only try once in GetIdFromUnsafeWidgetDefinitionAsync * Lock while recreating definition * ResetOopWidgetDefinitionAsync * async * fix lock --- .../ComSafeWidgetObjects/ComSafeHelpers.cs | 33 +++ .../ComSafeWidgetDefinition.cs | 215 ++++++++++++++++++ .../Controls/WidgetControl.xaml.cs | 22 +- .../Services/IWidgetIconService.cs | 6 +- .../Services/IWidgetScreenshotService.cs | 4 +- .../Services/WidgetIconService.cs | 9 +- .../Services/WidgetScreenshotService.cs | 7 +- .../ViewModels/AddWidgetViewModel.cs | 8 +- .../ViewModels/WidgetViewModel.cs | 12 +- .../Views/AddWidgetDialog.xaml.cs | 22 +- .../Views/DashboardView.xaml.cs | 103 +++++---- 11 files changed, 366 insertions(+), 75 deletions(-) create mode 100644 tools/Dashboard/DevHome.Dashboard/ComSafeWidgetObjects/ComSafeHelpers.cs create mode 100644 tools/Dashboard/DevHome.Dashboard/ComSafeWidgetObjects/ComSafeWidgetDefinition.cs diff --git a/tools/Dashboard/DevHome.Dashboard/ComSafeWidgetObjects/ComSafeHelpers.cs b/tools/Dashboard/DevHome.Dashboard/ComSafeWidgetObjects/ComSafeHelpers.cs new file mode 100644 index 0000000000..0977a31f68 --- /dev/null +++ b/tools/Dashboard/DevHome.Dashboard/ComSafeWidgetObjects/ComSafeHelpers.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DevHome.Dashboard.Services; + +namespace DevHome.Dashboard.ComSafeWidgetObjects; + +internal sealed class ComSafeHelpers +{ + public static async Task> GetAllOrderedComSafeWidgetDefinitions(IWidgetHostingService widgetHostingService) + { + var unsafeWidgetDefinitions = await widgetHostingService.GetWidgetDefinitionsAsync(); + var comSafeWidgetDefinitions = new List(); + foreach (var unsafeWidgetDefinition in unsafeWidgetDefinitions) + { + var id = await ComSafeWidgetDefinition.GetIdFromUnsafeWidgetDefinitionAsync(unsafeWidgetDefinition); + if (!string.IsNullOrEmpty(id)) + { + var comSafeWidgetDefinition = new ComSafeWidgetDefinition(id); + if (await comSafeWidgetDefinition.Populate()) + { + comSafeWidgetDefinitions.Add(comSafeWidgetDefinition); + } + } + } + + comSafeWidgetDefinitions = comSafeWidgetDefinitions.OrderBy(def => def.DisplayTitle).ToList(); + return comSafeWidgetDefinitions; + } +} diff --git a/tools/Dashboard/DevHome.Dashboard/ComSafeWidgetObjects/ComSafeWidgetDefinition.cs b/tools/Dashboard/DevHome.Dashboard/ComSafeWidgetObjects/ComSafeWidgetDefinition.cs new file mode 100644 index 0000000000..94b06aaab3 --- /dev/null +++ b/tools/Dashboard/DevHome.Dashboard/ComSafeWidgetObjects/ComSafeWidgetDefinition.cs @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using DevHome.Common.Extensions; +using DevHome.Dashboard.Services; +using Microsoft.UI.Xaml; +using Microsoft.Windows.Widgets.Hosts; +using Serilog; + +namespace DevHome.Dashboard.ComSafeWidgetObjects; + +/// +/// Since WidgetDefinitions are OOP COM objects, we need to wrap them in a safe way to handle COM exceptions +/// that arise when the underlying OOP object vanishes. All WidgetDefinitions should be wrapped in a +/// ComSafeWidgetDefinition and calls to the WidgetDefinition should be done through the ComSafeWidgetDefinition. +/// This class will handle the COM exceptions and get a new OOP WidgetDefinition if needed. +/// All APIs on the IWidgetDefinition and IWidgetDefinition2 interfaces are reflected here. +/// +public class ComSafeWidgetDefinition : IDisposable +{ + public bool AllowMultiple { get; private set; } + + public string Description { get; private set; } + + public string DisplayTitle { get; private set; } + + public string Id { get; private set; } + + public WidgetProviderDefinition ProviderDefinition { get; private set; } + + // Since the ProviderDefinition could have died, save the display name and id so we can use it in that case. + // These can be removed if we keep a ComSafeWidgetProviderDefinition here. + public string ProviderDefinitionDisplayName { get; private set; } + + public string ProviderDefinitionId { get; private set; } + + public string AdditionalInfoUri { get; private set; } + + public bool IsCustomizable { get; private set; } + + public string ProgressiveWebAppHostPackageFamilyName => throw new System.NotImplementedException(); + + public WidgetType Type { get; private set; } + + private WidgetDefinition _oopWidgetDefinition; + + private const int RpcServerUnavailable = unchecked((int)0x800706BA); + private const int RpcCallFailed = unchecked((int)0x800706BE); + + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ComSafeWidgetDefinition)); + + private readonly SemaphoreSlim _getDefinitionLock = new(1, 1); + private bool _disposedValue; + + private bool _hasValidProperties; + private const int MaxAttempts = 3; + + public ComSafeWidgetDefinition(string widgetDefinitionId) + { + Id = widgetDefinitionId; + } + + public async Task Populate() + { + await LazilyLoadOopWidgetDefinitionAsync(); + return _hasValidProperties; + } + + /// + /// Gets the theme resources from the widget. Tries multiple times in case of COM exceptions. + /// + /// The theme resources, or null in the case of failure. + public async Task GetThemeResourceAsync(WidgetTheme theme) + { + var attempt = 0; + while (attempt++ < MaxAttempts) + { + try + { + await LazilyLoadOopWidgetDefinitionAsync(); + return await Task.Run(() => _oopWidgetDefinition.GetThemeResource(theme)); + } + catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) + { + _log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + ResetOopWidgetDefinition(); + } + catch (Exception ex) + { + _log.Error(ex, "Exception getting theme resources from widget:"); + return null; + } + } + + return null; + } + + public async Task GetWidgetCapabilitiesAsync() + { + var attempt = 0; + while (attempt++ < MaxAttempts) + { + try + { + await LazilyLoadOopWidgetDefinitionAsync(); + return await Task.Run(() => _oopWidgetDefinition.GetWidgetCapabilities()); + } + catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) + { + _log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + ResetOopWidgetDefinition(); + } + catch (Exception ex) + { + _log.Error(ex, "Exception getting widget capabilities from widget:"); + return null; + } + } + + return null; + } + + private void ResetOopWidgetDefinition() + { + _oopWidgetDefinition = null; + _hasValidProperties = false; + } + + private async Task LazilyLoadOopWidgetDefinitionAsync() + { + var attempt = 0; + await _getDefinitionLock.WaitAsync(); + try + { + while (attempt++ < 3 && (_oopWidgetDefinition == null || _hasValidProperties == false)) + { + try + { + _oopWidgetDefinition ??= await Application.Current.GetService().GetWidgetDefinitionAsync(Id); + + if (!_hasValidProperties) + { + await Task.Run(() => + { + AllowMultiple = _oopWidgetDefinition.AllowMultiple; + Description = _oopWidgetDefinition.Description; + DisplayTitle = _oopWidgetDefinition.DisplayTitle; + Id = _oopWidgetDefinition.Id; + ProviderDefinition = _oopWidgetDefinition.ProviderDefinition; + ProviderDefinitionDisplayName = _oopWidgetDefinition.ProviderDefinition.DisplayName; + ProviderDefinitionId = _oopWidgetDefinition.ProviderDefinition.Id; + AdditionalInfoUri = _oopWidgetDefinition.AdditionalInfoUri; + IsCustomizable = _oopWidgetDefinition.IsCustomizable; + Type = _oopWidgetDefinition.Type; + _hasValidProperties = true; + }); + } + } + catch (Exception ex) + { + _log.Warning(ex, "Failed to get properties of out-of-proc object"); + } + } + } + finally + { + _getDefinitionLock.Release(); + } + } + + /// + /// Get a WidgetDefinition's ID from a WidgetDefinition object. + /// + /// WidgetDefinition + /// The Widget's Id, or in the case of failure string.Empty + public static async Task GetIdFromUnsafeWidgetDefinitionAsync(WidgetDefinition widgetDefinition) + { + return await Task.Run(() => + { + try + { + return widgetDefinition.Id; + } + catch (Exception ex) + { + Log.Warning(ex, $"Failed to operate on out-of-proc object with error code: 0x{ex.HResult:x}"); + } + + return string.Empty; + }); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _getDefinitionLock.Dispose(); + } + + _disposedValue = true; + } + } +} diff --git a/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs index 52b839993c..309e4c45d8 100644 --- a/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using DevHome.Common.Extensions; using DevHome.Common.Services; +using DevHome.Dashboard.ComSafeWidgetObjects; using DevHome.Dashboard.Services; using DevHome.Dashboard.ViewModels; using DevHome.Dashboard.Views; @@ -123,13 +124,28 @@ private async void OnRemoveWidgetClick(object sender, RoutedEventArgs e) private async Task AddSizesToWidgetMenuAsync(MenuFlyout widgetMenuFlyout, WidgetViewModel widgetViewModel) { - var widgetDefinition = await Application.Current.GetService().GetWidgetDefinitionAsync(widgetViewModel.Widget.DefinitionId); - if (widgetDefinition == null) + var unsafeWidgetDefinition = await Application.Current.GetService().GetWidgetDefinitionAsync(widgetViewModel.Widget.DefinitionId); + if (unsafeWidgetDefinition == null) { + // If we can't get the widgetDefinition, bail and don't show sizes. return; } - var capabilities = widgetDefinition.GetWidgetCapabilities(); + var widgetDefinitionId = await ComSafeWidgetDefinition.GetIdFromUnsafeWidgetDefinitionAsync(unsafeWidgetDefinition); + if (string.IsNullOrEmpty(widgetDefinitionId)) + { + // If we can't get the widgetDefinitionId, bail and don't show sizes. + return; + } + + var comSafeWidgetDefinition = new ComSafeWidgetDefinition(widgetDefinitionId); + if (!await comSafeWidgetDefinition.Populate()) + { + // If we can't populate the widgetDefinition, bail and don't show sizes. + return; + } + + var capabilities = await comSafeWidgetDefinition.GetWidgetCapabilitiesAsync(); var sizeMenuItems = new List(); // Add the three possible sizes. Each side should only be enabled if it is included in the widget's capabilities. diff --git a/tools/Dashboard/DevHome.Dashboard/Services/IWidgetIconService.cs b/tools/Dashboard/DevHome.Dashboard/Services/IWidgetIconService.cs index 7a4cdaade7..5a0506fc1f 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/IWidgetIconService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/IWidgetIconService.cs @@ -2,10 +2,10 @@ // Licensed under the MIT License. using System.Threading.Tasks; +using DevHome.Dashboard.ComSafeWidgetObjects; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; -using Microsoft.Windows.Widgets.Hosts; namespace DevHome.Dashboard.Services; @@ -13,7 +13,7 @@ public interface IWidgetIconService { public void RemoveIconsFromCache(string definitionId); - public Task GetIconFromCacheAsync(WidgetDefinition widgetDefinition, ElementTheme theme); + public Task GetIconFromCacheAsync(ComSafeWidgetDefinition widgetDefinition, ElementTheme theme); - public Task GetBrushForWidgetIconAsync(WidgetDefinition widgetDefinition, ElementTheme theme); + public Task GetBrushForWidgetIconAsync(ComSafeWidgetDefinition widgetDefinition, ElementTheme theme); } diff --git a/tools/Dashboard/DevHome.Dashboard/Services/IWidgetScreenshotService.cs b/tools/Dashboard/DevHome.Dashboard/Services/IWidgetScreenshotService.cs index de54cf5db3..0745806081 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/IWidgetScreenshotService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/IWidgetScreenshotService.cs @@ -2,9 +2,9 @@ // Licensed under the MIT License. using System.Threading.Tasks; +using DevHome.Dashboard.ComSafeWidgetObjects; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media.Imaging; -using Microsoft.Windows.Widgets.Hosts; namespace DevHome.Dashboard.Services; @@ -12,5 +12,5 @@ public interface IWidgetScreenshotService { public void RemoveScreenshotsFromCache(string definitionId); - public Task GetScreenshotFromCacheAsync(WidgetDefinition widgetDefinition, ElementTheme actualTheme); + public Task GetScreenshotFromCacheAsync(ComSafeWidgetDefinition widgetDefinition, ElementTheme actualTheme); } diff --git a/tools/Dashboard/DevHome.Dashboard/Services/WidgetIconService.cs b/tools/Dashboard/DevHome.Dashboard/Services/WidgetIconService.cs index 5b55df15b7..2d5b9015f3 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/WidgetIconService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/WidgetIconService.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Threading.Tasks; +using DevHome.Dashboard.ComSafeWidgetObjects; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; @@ -37,7 +38,7 @@ public void RemoveIconsFromCache(string definitionId) _widgetDarkIconCache.TryRemove(definitionId, out _); } - public async Task GetIconFromCacheAsync(WidgetDefinition widgetDefinition, ElementTheme theme) + public async Task GetIconFromCacheAsync(ComSafeWidgetDefinition widgetDefinition, ElementTheme theme) { var widgetDefinitionId = widgetDefinition.Id; BitmapImage bitmapImage; @@ -60,19 +61,19 @@ public async Task GetIconFromCacheAsync(WidgetDefinition widgetDefi // If the icon wasn't already in the cache, get it from the widget definition and add it to the cache before returning. if (theme == ElementTheme.Dark) { - bitmapImage = await WidgetIconToBitmapImageAsync(widgetDefinition.GetThemeResource(WidgetTheme.Dark).Icon); + bitmapImage = await WidgetIconToBitmapImageAsync((await widgetDefinition.GetThemeResourceAsync(WidgetTheme.Dark)).Icon); _widgetDarkIconCache.TryAdd(widgetDefinitionId, bitmapImage); } else { - bitmapImage = await WidgetIconToBitmapImageAsync(widgetDefinition.GetThemeResource(WidgetTheme.Light).Icon); + bitmapImage = await WidgetIconToBitmapImageAsync((await widgetDefinition.GetThemeResourceAsync(WidgetTheme.Light)).Icon); _widgetLightIconCache.TryAdd(widgetDefinitionId, bitmapImage); } return bitmapImage; } - public async Task GetBrushForWidgetIconAsync(WidgetDefinition widgetDefinition, ElementTheme theme) + public async Task GetBrushForWidgetIconAsync(ComSafeWidgetDefinition widgetDefinition, ElementTheme theme) { var image = await GetIconFromCacheAsync(widgetDefinition, theme); diff --git a/tools/Dashboard/DevHome.Dashboard/Services/WidgetScreenshotService.cs b/tools/Dashboard/DevHome.Dashboard/Services/WidgetScreenshotService.cs index 34a0da2bb1..63e1da92c5 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/WidgetScreenshotService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/WidgetScreenshotService.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using DevHome.Dashboard.ComSafeWidgetObjects; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.Windows.Widgets.Hosts; @@ -35,7 +36,7 @@ public void RemoveScreenshotsFromCache(string definitionId) _widgetDarkScreenshotCache.Remove(definitionId, out _); } - public async Task GetScreenshotFromCacheAsync(WidgetDefinition widgetDefinition, ElementTheme actualTheme) + public async Task GetScreenshotFromCacheAsync(ComSafeWidgetDefinition widgetDefinition, ElementTheme actualTheme) { var widgetDefinitionId = widgetDefinition.Id; BitmapImage bitmapImage; @@ -58,12 +59,12 @@ public async Task GetScreenshotFromCacheAsync(WidgetDefinition widg // If the screenshot wasn't already in the cache, get it from the widget definition and add it to the cache before returning. if (actualTheme == ElementTheme.Dark) { - bitmapImage = await WidgetScreenshotToBitmapImageAsync(widgetDefinition.GetThemeResource(WidgetTheme.Dark).GetScreenshots().FirstOrDefault().Image); + bitmapImage = await WidgetScreenshotToBitmapImageAsync((await widgetDefinition.GetThemeResourceAsync(WidgetTheme.Dark)).GetScreenshots().FirstOrDefault().Image); _widgetDarkScreenshotCache.TryAdd(widgetDefinitionId, bitmapImage); } else { - bitmapImage = await WidgetScreenshotToBitmapImageAsync(widgetDefinition.GetThemeResource(WidgetTheme.Light).GetScreenshots().FirstOrDefault().Image); + bitmapImage = await WidgetScreenshotToBitmapImageAsync((await widgetDefinition.GetThemeResourceAsync(WidgetTheme.Light)).GetScreenshots().FirstOrDefault().Image); _widgetLightScreenshotCache.TryAdd(widgetDefinitionId, bitmapImage); } diff --git a/tools/Dashboard/DevHome.Dashboard/ViewModels/AddWidgetViewModel.cs b/tools/Dashboard/DevHome.Dashboard/ViewModels/AddWidgetViewModel.cs index 6bb7583b8a..dafb05165b 100644 --- a/tools/Dashboard/DevHome.Dashboard/ViewModels/AddWidgetViewModel.cs +++ b/tools/Dashboard/DevHome.Dashboard/ViewModels/AddWidgetViewModel.cs @@ -5,9 +5,9 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DevHome.Contracts.Services; +using DevHome.Dashboard.ComSafeWidgetObjects; using DevHome.Dashboard.Services; using Microsoft.UI.Xaml.Media; -using Microsoft.Windows.Widgets.Hosts; namespace DevHome.Dashboard.ViewModels; @@ -28,7 +28,7 @@ public partial class AddWidgetViewModel : ObservableObject [ObservableProperty] private bool _pinButtonVisibility; - private WidgetDefinition _selectedWidgetDefinition; + private ComSafeWidgetDefinition _selectedWidgetDefinition; public AddWidgetViewModel( IWidgetScreenshotService widgetScreenshotService, @@ -38,13 +38,13 @@ public AddWidgetViewModel( _themeSelectorService = themeSelectorService; } - public async Task SetWidgetDefinition(WidgetDefinition selectedWidgetDefinition) + public async Task SetWidgetDefinition(ComSafeWidgetDefinition selectedWidgetDefinition) { _selectedWidgetDefinition = selectedWidgetDefinition; var bitmap = await _widgetScreenshotService.GetScreenshotFromCacheAsync(selectedWidgetDefinition, _themeSelectorService.GetActualTheme()); WidgetDisplayTitle = selectedWidgetDefinition.DisplayTitle; - WidgetProviderDisplayTitle = selectedWidgetDefinition.ProviderDefinition.DisplayName; + WidgetProviderDisplayTitle = selectedWidgetDefinition.ProviderDefinitionDisplayName; WidgetScreenshot = new ImageBrush { ImageSource = bitmap, diff --git a/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs b/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs index 8e2ba72257..2a83d962be 100644 --- a/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs +++ b/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs @@ -35,7 +35,7 @@ namespace DevHome.Dashboard.ViewModels; public delegate WidgetViewModel WidgetViewModelFactory( ComSafeWidget widget, WidgetSize widgetSize, - WidgetDefinition widgetDefinition); + ComSafeWidgetDefinition widgetDefinition); public partial class WidgetViewModel : ObservableObject { @@ -50,7 +50,7 @@ public partial class WidgetViewModel : ObservableObject private ComSafeWidget _widget; [ObservableProperty] - private WidgetDefinition _widgetDefinition; + private ComSafeWidgetDefinition _widgetDefinition; [ObservableProperty] private WidgetSize _widgetSize; @@ -84,12 +84,12 @@ partial void OnWidgetChanged(ComSafeWidget value) } } - partial void OnWidgetDefinitionChanged(WidgetDefinition value) + partial void OnWidgetDefinitionChanged(ComSafeWidgetDefinition value) { if (WidgetDefinition != null) { WidgetDisplayTitle = WidgetDefinition.DisplayTitle; - WidgetProviderDisplayTitle = WidgetDefinition.ProviderDefinition.DisplayName; + WidgetProviderDisplayTitle = WidgetDefinition.ProviderDefinitionDisplayName; IsCustomizable = WidgetDefinition.IsCustomizable; } } @@ -97,7 +97,7 @@ partial void OnWidgetDefinitionChanged(WidgetDefinition value) public WidgetViewModel( ComSafeWidget widget, WidgetSize widgetSize, - WidgetDefinition widgetDefinition, + ComSafeWidgetDefinition widgetDefinition, WidgetAdaptiveCardRenderingService adaptiveCardRenderingService, WindowEx windowEx) { @@ -324,7 +324,7 @@ private async void HandleAdaptiveAction(RenderedAdaptiveCard sender, AdaptiveAct TelemetryFactory.Get().Log( "Dashboard_ReportWidgetInteraction", LogLevel.Critical, - new ReportWidgetInteractionEvent(WidgetDefinition.ProviderDefinition.Id, WidgetDefinition.Id, args.Action.ActionTypeString)); + new ReportWidgetInteractionEvent(WidgetDefinition.ProviderDefinitionId, WidgetDefinition.Id, args.Action.ActionTypeString)); // TODO: Handle other ActionTypes // https://github.com/microsoft/devhome/issues/644 diff --git a/tools/Dashboard/DevHome.Dashboard/Views/AddWidgetDialog.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Views/AddWidgetDialog.xaml.cs index 750682027c..214ba7c386 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/AddWidgetDialog.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Views/AddWidgetDialog.xaml.cs @@ -28,9 +28,9 @@ public sealed partial class AddWidgetDialog : ContentDialog { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(AddWidgetDialog)); - private WidgetDefinition _selectedWidget; + private ComSafeWidgetDefinition _selectedWidget; - public WidgetDefinition AddedWidget { get; private set; } + public ComSafeWidgetDefinition AddedWidget { get; private set; } public AddWidgetViewModel ViewModel { get; set; } @@ -78,9 +78,9 @@ private async Task FillAvailableWidgetsAsync() // Show the providers and widgets underneath them in alphabetical order. var providerDefinitions = (await _hostingService.GetProviderDefinitionsAsync()).OrderBy(x => x.DisplayName); - var widgetDefinitions = (await _hostingService.GetWidgetDefinitionsAsync()).OrderBy(x => x.DisplayTitle); + var comSafeWidgetDefinitions = await ComSafeHelpers.GetAllOrderedComSafeWidgetDefinitions(_hostingService); - _log.Information($"Filling available widget list, found {providerDefinitions.Count()} providers and {widgetDefinitions.Count()} widgets"); + _log.Information($"Filling available widget list, found {providerDefinitions.Count()} providers and {comSafeWidgetDefinitions.Count} widgets"); // Fill NavigationView Menu with Widget Providers, and group widgets under each provider. // Tag each item with the widget or provider definition, so that it can be used to create @@ -107,9 +107,9 @@ private async Task FillAvailableWidgetsAsync() Content = providerDef.DisplayName, }; - foreach (var widgetDef in widgetDefinitions) + foreach (var widgetDef in comSafeWidgetDefinitions) { - if (widgetDef.ProviderDefinition.Id.Equals(providerDef.Id, StringComparison.Ordinal)) + if (widgetDef.ProviderDefinitionId.Equals(providerDef.Id, StringComparison.Ordinal)) { var subItemContent = await BuildWidgetNavItem(widgetDef); var enable = !IsSingleInstanceAndAlreadyPinned(widgetDef, [.. comSafeCurrentlyPinnedWidgets]); @@ -141,7 +141,7 @@ private async Task FillAvailableWidgetsAsync() } } - private async Task BuildWidgetNavItem(WidgetDefinition widgetDefinition) + private async Task BuildWidgetNavItem(ComSafeWidgetDefinition widgetDefinition) { var image = await _widgetIconService.GetIconFromCacheAsync(widgetDefinition, ActualTheme); return BuildNavItem(image, widgetDefinition.DisplayTitle); @@ -180,7 +180,7 @@ private StackPanel BuildNavItem(BitmapImage image, string text) return itemContent; } - private bool IsSingleInstanceAndAlreadyPinned(WidgetDefinition widgetDef, ComSafeWidget[] currentlyPinnedWidgets) + private bool IsSingleInstanceAndAlreadyPinned(ComSafeWidgetDefinition widgetDef, ComSafeWidget[] currentlyPinnedWidgets) { // If a WidgetDefinition has AllowMultiple = false, only one of that widget can be pinned at one time. if (!widgetDef.AllowMultiple) @@ -235,7 +235,7 @@ private async void AddWidgetNavigationView_SelectionChanged( } // If the user has selected a widget, show preview. If they selected a provider, leave space blank. - if (selectedTag as WidgetDefinition is WidgetDefinition selectedWidgetDefinition) + if (selectedTag as ComSafeWidgetDefinition is ComSafeWidgetDefinition selectedWidgetDefinition) { _selectedWidget = selectedWidgetDefinition; await ViewModel.SetWidgetDefinition(selectedWidgetDefinition); @@ -254,7 +254,7 @@ private async Task UpdateThemeAsync() { foreach (var widgetItem in providerItem.MenuItems.OfType()) { - if (widgetItem.Tag is WidgetDefinition widgetDefinition) + if (widgetItem.Tag is ComSafeWidgetDefinition widgetDefinition) { var image = await _widgetIconService.GetIconFromCacheAsync(widgetDefinition, ActualTheme); widgetItem.Content = BuildNavItem(image, widgetDefinition.DisplayTitle); @@ -321,7 +321,7 @@ private void WidgetCatalog_WidgetDefinitionDeleted(WidgetCatalog sender, WidgetD { foreach (var widgetItem in providerItem.MenuItems.OfType()) { - if (widgetItem.Tag is WidgetDefinition tagDefinition) + if (widgetItem.Tag is ComSafeWidgetDefinition tagDefinition) { if (tagDefinition.Id.Equals(deletedDefinitionId, StringComparison.Ordinal)) { diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs index 70bcf4009e..817b63fed7 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs @@ -86,9 +86,6 @@ private async Task SubscribeToWidgetCatalogEventsAsync() return false; } - widgetCatalog!.WidgetProviderDefinitionAdded += WidgetCatalog_WidgetProviderDefinitionAdded; - widgetCatalog!.WidgetProviderDefinitionDeleted += WidgetCatalog_WidgetProviderDefinitionDeleted; - widgetCatalog!.WidgetDefinitionAdded += WidgetCatalog_WidgetDefinitionAdded; widgetCatalog!.WidgetDefinitionUpdated += WidgetCatalog_WidgetDefinitionUpdated; widgetCatalog!.WidgetDefinitionDeleted += WidgetCatalog_WidgetDefinitionDeleted; } @@ -113,9 +110,6 @@ private async Task UnsubscribeFromWidgetCatalogEventsAsync() return; } - widgetCatalog!.WidgetProviderDefinitionAdded -= WidgetCatalog_WidgetProviderDefinitionAdded; - widgetCatalog!.WidgetProviderDefinitionDeleted -= WidgetCatalog_WidgetProviderDefinitionDeleted; - widgetCatalog!.WidgetDefinitionAdded -= WidgetCatalog_WidgetDefinitionAdded; widgetCatalog!.WidgetDefinitionUpdated -= WidgetCatalog_WidgetDefinitionUpdated; widgetCatalog!.WidgetDefinitionDeleted -= WidgetCatalog_WidgetDefinitionDeleted; } @@ -299,15 +293,24 @@ private async Task RestorePinnedWidgetsAsync(ComSafeWidget[] hostWidgets) continue; } - // Ensure only one copy of a widget is pinned if that widget's definition only allows for one instance. var widgetDefinitionId = widget.DefinitionId; - var widgetDefinition = await ViewModel.WidgetHostingService.GetWidgetDefinitionAsync(widgetDefinitionId); - if (widgetDefinition == null) + var unsafeWidgetDefinition = await ViewModel.WidgetHostingService.GetWidgetDefinitionAsync(widgetDefinitionId); + if (unsafeWidgetDefinition == null) + { + await DeleteWidgetWithNoDefinition(widget, widgetDefinitionId); + continue; + } + + // Ensure only one copy of a widget is pinned if that widget's definition only allows for one instance. + var comSafeWidgetDefinition = new ComSafeWidgetDefinition(widgetDefinitionId); + if (!await comSafeWidgetDefinition.Populate()) { + _log.Error($"Error populating widget definition for widget {widgetDefinitionId}"); await DeleteWidgetWithNoDefinition(widget, widgetDefinitionId); + continue; } - if (widgetDefinition.AllowMultiple == false) + if (comSafeWidgetDefinition.AllowMultiple == false) { if (pinnedSingleInstanceWidgets.Contains(widgetDefinitionId)) { @@ -383,24 +386,24 @@ private async Task DeleteAbandonedWidgetAsync(ComSafeWidget widget) private async Task PinDefaultWidgetsAsync() { - var widgetDefinitions = (await ViewModel.WidgetHostingService.GetWidgetDefinitionsAsync()).OrderBy(x => x.DisplayTitle); - foreach (var widgetDefinition in widgetDefinitions) + var comSafeWidgetDefinitions = await ComSafeHelpers.GetAllOrderedComSafeWidgetDefinitions(ViewModel.WidgetHostingService); + foreach (var comSafeWidgetDefinition in comSafeWidgetDefinitions) { - var id = widgetDefinition.Id; + var id = comSafeWidgetDefinition.Id; if (WidgetHelpers.DefaultWidgetDefinitionIds.Contains(id)) { _log.Information($"Found default widget {id}"); - await PinDefaultWidgetAsync(widgetDefinition); + await PinDefaultWidgetAsync(comSafeWidgetDefinition); } } } - private async Task PinDefaultWidgetAsync(WidgetDefinition defaultWidgetDefinition) + private async Task PinDefaultWidgetAsync(ComSafeWidgetDefinition defaultWidgetDefinition) { try { // Create widget - var size = WidgetHelpers.GetDefaultWidgetSize(defaultWidgetDefinition.GetWidgetCapabilities()); + var size = WidgetHelpers.GetDefaultWidgetSize(await defaultWidgetDefinition.GetWidgetCapabilitiesAsync()); var definitionId = defaultWidgetDefinition.Id; var unsafeWidget = await ViewModel.WidgetHostingService.CreateWidgetAsync(definitionId, size); if (unsafeWidget == null) @@ -477,7 +480,7 @@ public async Task AddWidgetClickAsync() { try { - var size = WidgetHelpers.GetDefaultWidgetSize(newWidgetDefinition.GetWidgetCapabilities()); + var size = WidgetHelpers.GetDefaultWidgetSize(await newWidgetDefinition.GetWidgetCapabilitiesAsync()); var unsafeWidget = await ViewModel.WidgetHostingService.CreateWidgetAsync(newWidgetDefinition.Id, size); if (unsafeWidget == null) { @@ -541,18 +544,25 @@ await Task.Run(async () => { var widgetDefinitionId = widget.DefinitionId; var widgetId = widget.Id; - var widgetDefinition = await ViewModel.WidgetHostingService.GetWidgetDefinitionAsync(widgetDefinitionId); + _log.Information($"Insert widget in pinned widgets, id = {widgetId}, index = {index}"); - if (widgetDefinition != null) + var unsafeWidgetDefinition = await ViewModel.WidgetHostingService.GetWidgetDefinitionAsync(widgetDefinitionId); + if (unsafeWidgetDefinition != null) { - _log.Information($"Insert widget in pinned widgets, id = {widgetId}, index = {index}"); + var comSafeWidgetDefinition = new ComSafeWidgetDefinition(widgetDefinitionId); + if (!await comSafeWidgetDefinition.Populate()) + { + _log.Error($"Error inserting widget in pinned widgets, id = {widgetId}, index = {index}"); + await widget.DeleteAsync(); + return; + } TelemetryFactory.Get().Log( "Dashboard_ReportPinnedWidget", LogLevel.Critical, - new ReportPinnedWidgetEvent(widgetDefinition.ProviderDefinition.Id, widgetDefinitionId)); + new ReportPinnedWidgetEvent(comSafeWidgetDefinition.ProviderDefinitionId, widgetDefinitionId)); - var wvm = _widgetViewModelFactory(widget, size, widgetDefinition); + var wvm = _widgetViewModelFactory(widget, size, comSafeWidgetDefinition); _windowEx.DispatcherQueue.TryEnqueue(() => { try @@ -589,18 +599,34 @@ private async Task DeleteWidgetWithNoDefinition(ComSafeWidget widget, string wid } } - private void WidgetCatalog_WidgetProviderDefinitionAdded(WidgetCatalog sender, WidgetProviderDefinitionAddedEventArgs args) => - _log.Information("DashboardView", $"WidgetCatalog_WidgetProviderDefinitionAdded {args.ProviderDefinition.Id}"); + private async void WidgetCatalog_WidgetDefinitionUpdated(WidgetCatalog sender, WidgetDefinitionUpdatedEventArgs args) + { + WidgetDefinition unsafeWidgetDefinition; + try + { + unsafeWidgetDefinition = await Task.Run(() => args.Definition); + } + catch (Exception ex) + { + _log.Error(ex, "WidgetCatalog_WidgetDefinitionUpdated: Couldn't get args.WidgetDefinition"); + return; + } - private void WidgetCatalog_WidgetProviderDefinitionDeleted(WidgetCatalog sender, WidgetProviderDefinitionDeletedEventArgs args) => - _log.Information("DashboardView", $"WidgetCatalog_WidgetProviderDefinitionDeleted {args.ProviderDefinitionId}"); + if (unsafeWidgetDefinition == null) + { + _log.Error("WidgetCatalog_WidgetDefinitionUpdated: Couldn't get WidgetDefinition"); + return; + } - private void WidgetCatalog_WidgetDefinitionAdded(WidgetCatalog sender, WidgetDefinitionAddedEventArgs args) => - _log.Information("DashboardView", $"WidgetCatalog_WidgetDefinitionAdded {args.Definition.Id}"); + var widgetDefinitionId = await ComSafeWidgetDefinition.GetIdFromUnsafeWidgetDefinitionAsync(unsafeWidgetDefinition); + var comSafeNewDefinition = new ComSafeWidgetDefinition(widgetDefinitionId); + if (!await comSafeNewDefinition.Populate()) + { + _log.Error($"Error populating widget definition for widget {widgetDefinitionId}"); + return; + } - private async void WidgetCatalog_WidgetDefinitionUpdated(WidgetCatalog sender, WidgetDefinitionUpdatedEventArgs args) - { - var updatedDefinitionId = args.Definition.Id; + var updatedDefinitionId = comSafeNewDefinition.Id; _log.Information($"WidgetCatalog_WidgetDefinitionUpdated {updatedDefinitionId}"); var matchingWidgetsFound = 0; @@ -610,14 +636,13 @@ private async void WidgetCatalog_WidgetDefinitionUpdated(WidgetCatalog sender, W // Things in the definition that we need to update to if they have changed: // AllowMultiple, DisplayTitle, Capabilities (size), ThemeResource (icons) var oldDef = widgetToUpdate.WidgetDefinition; - var newDef = args.Definition; // If we're no longer allowed to have multiple instances of this widget, delete all but the first. - if (++matchingWidgetsFound > 1 && newDef.AllowMultiple == false && oldDef.AllowMultiple == true) + if (++matchingWidgetsFound > 1 && comSafeNewDefinition.AllowMultiple == false && oldDef.AllowMultiple == true) { _windowEx.DispatcherQueue.TryEnqueue(async () => { - _log.Information($"No longer allowed to have multiple of widget {newDef.Id}"); + _log.Information($"No longer allowed to have multiple of widget {updatedDefinitionId}"); _log.Information($"Delete widget {widgetToUpdate.Widget.Id}"); PinnedWidgets.Remove(widgetToUpdate); await widgetToUpdate.Widget.DeleteAsync(); @@ -627,19 +652,19 @@ private async void WidgetCatalog_WidgetDefinitionUpdated(WidgetCatalog sender, W else { // Changing the definition updates the DisplayTitle. - widgetToUpdate.WidgetDefinition = newDef; + widgetToUpdate.WidgetDefinition = comSafeNewDefinition; // If the size the widget is currently set to is no longer supported by the widget, revert to its default size. // TODO: Need to update WidgetControl with now-valid sizes. // TODO: Properly compare widget capabilities. // https://github.com/microsoft/devhome/issues/641 - if (oldDef.GetWidgetCapabilities() != newDef.GetWidgetCapabilities()) + if (await oldDef.GetWidgetCapabilitiesAsync() != await comSafeNewDefinition.GetWidgetCapabilitiesAsync()) { // TODO: handle the case where this change is made while Dev Home is not running -- how do we restore? // https://github.com/microsoft/devhome/issues/641 - if (!newDef.GetWidgetCapabilities().Any(cap => cap.Size == widgetToUpdate.WidgetSize)) + if (!(await comSafeNewDefinition.GetWidgetCapabilitiesAsync()).Any(cap => cap.Size == widgetToUpdate.WidgetSize)) { - var newDefaultSize = WidgetHelpers.GetDefaultWidgetSize(newDef.GetWidgetCapabilities()); + var newDefaultSize = WidgetHelpers.GetDefaultWidgetSize(await comSafeNewDefinition.GetWidgetCapabilitiesAsync()); widgetToUpdate.WidgetSize = newDefaultSize; await widgetToUpdate.Widget.SetSizeAsync(newDefaultSize); } @@ -673,7 +698,7 @@ private void WidgetCatalog_WidgetDefinitionDeleted(WidgetCatalog sender, WidgetD } // If a widget is removed from the list, update the saved positions of the following widgets. - // If not updated, widges pinned later may be assigned the same position as existing widgets, + // If not updated, widgets pinned later may be assigned the same position as existing widgets, // since the saved position may be greater than the number of pinned widgets. // Unsubscribe from this event during drag and drop, since the drop event takes care of re-numbering. private async void OnPinnedWidgetsCollectionChangedAsync(object sender, NotifyCollectionChangedEventArgs e) From a3dae8dca42b4141e268e5bb7673feceacbe25a7 Mon Sep 17 00:00:00 2001 From: Branden Bonaby <105318831+bbonaby@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:21:23 -0700 Subject: [PATCH 115/121] Fix Hyper-V extension launching vmconnect.exe behind the Dev Home window when the user launches a VM (#2658) * make sure vmconnect launches in the foreground * update comment * remove unused using * fix build --- .../HyperVExtension/Helpers/HyperVStrings.cs | 3 -- .../Models/HyperVVirtualMachine.cs | 26 ++++++++++++++++- .../src/HyperVExtension/NativeMethods.txt | 3 +- .../HyperVExtension/Services/HyperVManager.cs | 28 ------------------- .../Services/IHyperVManager.cs | 4 --- .../Services/HyperVManagerTest.cs | 18 ------------ 6 files changed, 27 insertions(+), 55 deletions(-) diff --git a/HyperVExtension/src/HyperVExtension/Helpers/HyperVStrings.cs b/HyperVExtension/src/HyperVExtension/Helpers/HyperVStrings.cs index dbe8cd76f5..0a78189893 100644 --- a/HyperVExtension/src/HyperVExtension/Helpers/HyperVStrings.cs +++ b/HyperVExtension/src/HyperVExtension/Helpers/HyperVStrings.cs @@ -96,7 +96,4 @@ public static class HyperVStrings public const string RunningState = "Running"; public const string PausedState = "Paused"; public const string SavedState = "Saved"; - - // Hyper-V scripts - public const string VmConnectScript = "vmconnect.exe localhost -G"; } diff --git a/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs b/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs index d27b3a45d5..b154cb3938 100644 --- a/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs +++ b/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Data; +using System.Diagnostics; using System.Globalization; using System.Management.Automation; using System.Runtime.InteropServices.WindowsRuntime; @@ -18,6 +19,7 @@ using Windows.Foundation; using Windows.Storage; using Windows.Storage.Streams; +using Windows.Win32; using Windows.Win32.Foundation; using SDK = Microsoft.Windows.DevHome.SDK; @@ -451,7 +453,29 @@ public IAsyncOperation ConnectAsync(string options { try { - _hyperVManager.ConnectToVirtualMachine(VmId); + _log.Information($"Starting vmconnect launch attempt on {DateTime.Now}: VM details: {this}"); + ProcessStartInfo processStartInfo = new ProcessStartInfo("vmconnect.exe"); + processStartInfo.UseShellExecute = true; + + // The -G flag allows us to use the Id of the VM to tell vmconnect.exe which VM to launch into. + processStartInfo.Arguments = $"localhost -G {Id}"; + + using (Process vmConnectProcess = new Process()) + { + vmConnectProcess.StartInfo = processStartInfo; + + // We start the process and will return success if it does not throw an exception. + // If vmconnect has an error, it will be displayed to the user in a message dialog + // outside of our control. + vmConnectProcess.Start(); + + // Note: Just because the vmconnect.exe launches in the foreground does not mean it will launch + // in front of the Dev Home window. Since the vmconnect.exe is a separate process being launched + // outside of Dev Home, it will not be parented to the Dev Home window. The shell will launch it + // in its last known location. + PInvoke.SetForegroundWindow((HWND)vmConnectProcess.MainWindowHandle); + } + _log.Information($"Successful vmconnect launch attempt on {DateTime.Now}: VM details: {this}"); return new ComputeSystemOperationResult(); } diff --git a/HyperVExtension/src/HyperVExtension/NativeMethods.txt b/HyperVExtension/src/HyperVExtension/NativeMethods.txt index 539d71be02..d263560343 100644 --- a/HyperVExtension/src/HyperVExtension/NativeMethods.txt +++ b/HyperVExtension/src/HyperVExtension/NativeMethods.txt @@ -5,4 +5,5 @@ E_ABORT S_OK MAX_PATH SFBS_FLAGS -StrFormatByteSizeEx \ No newline at end of file +StrFormatByteSizeEx +SetForegroundWindow \ No newline at end of file diff --git a/HyperVExtension/src/HyperVExtension/Services/HyperVManager.cs b/HyperVExtension/src/HyperVExtension/Services/HyperVManager.cs index 419f4233a6..9b81ce980e 100644 --- a/HyperVExtension/src/HyperVExtension/Services/HyperVManager.cs +++ b/HyperVExtension/src/HyperVExtension/Services/HyperVManager.cs @@ -432,34 +432,6 @@ public bool RemoveVirtualMachine(Guid vmId) } } - /// - public void ConnectToVirtualMachine(Guid vmId) - { - StartVirtualMachineManagementService(); - AddVirtualMachineToOperationsMap(vmId); - - try - { - // Build command line statement to connect to the VM. - var commandLineStatements = new StatementBuilder() - .AddScript($"{HyperVStrings.VmConnectScript} {vmId}", true) - .Build(); - - var result = _powerShellService.Execute(commandLineStatements, PipeType.PipeOutput); - - // Note: We use the vmconnect application to connect to the VM. VM connect will display a message box with - // an error if one occurs. We will only throw this error if an error occurs within the PowerShell session. - if (!string.IsNullOrEmpty(result.CommandOutputErrorMessage)) - { - throw new HyperVManagerException($"Unable to launch VM with Id {vmId} due to PowerShell error: {result.CommandOutputErrorMessage}"); - } - } - finally - { - RemoveVirtualMachineFromOperationsMap(vmId); - } - } - /// public IEnumerable GetVirtualMachineCheckpoints(Guid vmId) { diff --git a/HyperVExtension/src/HyperVExtension/Services/IHyperVManager.cs b/HyperVExtension/src/HyperVExtension/Services/IHyperVManager.cs index 43be65449f..30b1bf39ff 100644 --- a/HyperVExtension/src/HyperVExtension/Services/IHyperVManager.cs +++ b/HyperVExtension/src/HyperVExtension/Services/IHyperVManager.cs @@ -63,10 +63,6 @@ public interface IHyperVManager /// True if the virtual machine was removed successfully, false otherwise. public bool RemoveVirtualMachine(Guid vmId); - /// Starts a remote session a Hyper-V virtual machine. - /// The id of the virtual machine. - public void ConnectToVirtualMachine(Guid vmId); - /// Gets a list of all checkpoints for a Hyper-V virtual machine. /// The id of the virtual machine. /// A list of checkpoints and their parent checkpoints diff --git a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVManagerTest.cs b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVManagerTest.cs index e56427fdee..ef36c64bf4 100644 --- a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVManagerTest.cs +++ b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVManagerTest.cs @@ -302,24 +302,6 @@ public void RemoveVirtualMachineCanRemoveAVirtualMachine() Assert.IsTrue(wasVMRemoved); } - [TestMethod] - public void ConnectToVirtualMachineDoesNotThrow() - { - // Arrange - SetupHyperVTestMethod(HyperVStrings.HyperVModuleName, ServiceControllerStatus.Running); - var hyperVManager = TestHost.GetService(); - SetupPowerShellSessionInvokeResults() - .Returns(() => { return CreatePSObjectCollection(PowerShellHyperVModule); }) - .Returns(() => - { - // Simulate PowerShell won't return an object here, so simulate this. - return CreatePSObjectCollection(new PSCustomObjectMock()); - }); - - // This should not throw an exception - hyperVManager.ConnectToVirtualMachine(Guid.NewGuid()); - } - [TestMethod] public void GetVirtualMachineCheckpointsReturnCheckpoints() { From 867a83a27e85cb439c509e5537e3294bbd12659c Mon Sep 17 00:00:00 2001 From: sshilov7 <51001703+sshilov7@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:16:03 -0700 Subject: [PATCH 116/121] Update "Info" icon in Configuration Loading page. (#2661) --- .../DevHome.SetupFlow/Assets/DarkInfo.png | Bin 0 -> 1284 bytes .../DevHome.SetupFlow/Assets/LightInfo.png | Bin 0 -> 1405 bytes .../ViewModels/LoadingViewModel.cs | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Assets/DarkInfo.png create mode 100644 tools/SetupFlow/DevHome.SetupFlow/Assets/LightInfo.png diff --git a/tools/SetupFlow/DevHome.SetupFlow/Assets/DarkInfo.png b/tools/SetupFlow/DevHome.SetupFlow/Assets/DarkInfo.png new file mode 100644 index 0000000000000000000000000000000000000000..93aecaa36632cf36835f5af8a86a069c6cb59b38 GIT binary patch literal 1284 zcmV+f1^fDmP)@~0drDELIAGL9O(c600d`2O+f$vv5yPl8G8Wot8VfCgT^cq2K@hbD%Tz#Z5vctE{s+M-U1+qGbXw&cTb-fO zNulQZy;&H*<2c~KTd6`C& z-SMb{wmRAzeE$p2*Kn_GZ*M!Ik(g)zMkjFFg2A8VwW0mu;HpB9Q;1QOkO@ry7|pku zhS3?^r$rAAt~|u3hUFPU3lNdvozx*h8TaC_LJTDUjHlr_iO%p&99;RGot?4_$Up)x zupwBLa3y3kaNrT^gLs1_eWKp_

#1IMQ4mA0KD>{Js7>0LFh)eSKtsC*`Jm4S z769s&a7)T~P)1zX4)S0i0ZvX%=<>@lMj)xGrBbO6y`JkC1Ni30&3MB9S$VB5z8-)+ zpH;(Ls9axPOQlkg+uK`tczBS7g#}q#TXV0lK};whChT`>?<#F63x?Ej`w~duuVrU&_SCQ z#9JD)_5D^j*3Zn$$i>BlTwPtcpV964IR%OccJ&;;aT6h~;i;j|7+~-E-rio-Qa^kV z!7U8 znVg)I`T2RtWHQ>8#nG+5;_d(NYV$3WJUTjJcSy?^!t=_O0}_FTh{|5mks4Lq>UA&j2bE< zJ2(l$i}858pk}cH6XwxbTUDFt^oY<5Z=mJy^x@%Q0v?gX^-)4NJk3R0u3I*ivBE@) u0IjGlvaE`+mVz9g=WZ1@Y{EPw-HpE~dnrxL<@~0drDELIAGL9O(c600d`2O+f$vv5yPXqOY7<|O0r>EDcN~%%-x4O=){+R_2QGH-=w-~I(8Z@d9v^ooa zP)EW8{qgbfU}Qdu8>mZ!!O9`uQ4x5d^1xX&+$KJO}> zot-o=FhK3??Gy|K>Fn%`4i68_!)@3vta1byDOFz535qfPFLD_OJ~A>wt*x!ZfsA<% z*WKOScvw>zF7rMd4qv5Gsei2sExQ25c^NI=+uKY1{rx4&Q3mU98&>QBWU?x=Q~-~u zUt~mgP(V3gAre5iZs`x+D1bfwIMqIbv&2d3c64-*3=p_k1xiH<5N5JKlDNCOD_f_f zrG;c6xU5IC#l=P0I;W?nB$2jQ2lziJ$kWr) zj~ah8;#Aia@WMuE1u%UYURja4=*_GEOnu2qF#ktRAeRt9@Pjzu>gtNTgk}Fh^ZKO* zgdZClGiQbn9$=b0Iy!2Wc?o34FsPG;Di)~6_9}fGNdRx8OmTXlqN;tG(bk1 zo0~I8&5^)>?728#o75wXk6cc0ixh)p-oV*Z0J5Ws#REoe)oDHy=EUe4sY5Jxkyk(< z5Li{j3N8yrzPth=k;tVYRfUXBH3*AqpYs5v$JDq1^4&&W@cChkcCZu(5lRGncVK~ z?qxU}zQ@{Iav2DEe0*%CzmN#!2cz@zbK2b8l$I9fS>N33?CjrGWwr|?y$C{?Sr#jm;f}sxH-p=7!{*J0Z>;}c6 zDxK;81a633UPpP3eoHkV))ixUolf5Yv^qj}M6XmO6?K8vuwTdw4GnE`mw<2hi9G5S z!rYf`Zf<^`pPx@t4Y;;-Hn&NdBwB=M@wMCA+tp|^>Po0CZSypCnDGPuLN08c=KtDe zpj9~NU_J(5_GW4V1^m^K7H;ck^d=B66QRtaAL1Ku` Date: Thu, 18 Apr 2024 13:23:01 -0700 Subject: [PATCH 117/121] Change GetWidgetsAsync to always return a valid list (#2687) --------- Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com> --- .../Services/IWidgetHostingService.cs | 17 ++++++++ .../Services/WidgetHostingService.cs | 40 +++++-------------- .../Views/DashboardView.xaml.cs | 12 +++--- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/tools/Dashboard/DevHome.Dashboard/Services/IWidgetHostingService.cs b/tools/Dashboard/DevHome.Dashboard/Services/IWidgetHostingService.cs index 4abcd16923..db2cd091b3 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/IWidgetHostingService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/IWidgetHostingService.cs @@ -9,17 +9,34 @@ namespace DevHome.Dashboard.Services; public interface IWidgetHostingService { + ///

Get the list of current widgets from the WidgetService. + /// A list of widgets, or empty list if there were no widgets or the list could not be retrieved. public Task GetWidgetsAsync(); + /// Gets the widget with the given ID. + /// The widget, or null if one could not be retrieved. public Task GetWidgetAsync(string widgetId); + /// Create and return a new widget. + /// The new widget, or null if one could not be created. public Task CreateWidgetAsync(string widgetDefinitionId, WidgetSize widgetSize); + /// Get the catalog of widgets from the WidgetService. + /// The catalog of widgets, or null if one could not be created. public Task GetWidgetCatalogAsync(); + /// Get the list of WidgetProviderDefinitions from the WidgetService. + /// A list of WidgetProviderDefinitions, or an empty list if there were no widgets + /// or the list could not be retrieved. public Task GetProviderDefinitionsAsync(); + /// Get the list of WidgetDefinitions from the WidgetService. + /// A list of WidgetDefinitions, or an empty list if there were no widgets + /// or the list could not be retrieved. public Task GetWidgetDefinitionsAsync(); + /// Get the WidgetDefinition for the given WidgetDefinitionId from the WidgetService. + /// The WidgetDefinition, or null if the widget definition could not be found + /// or there was an error retrieving it. public Task GetWidgetDefinitionAsync(string widgetDefinitionId); } diff --git a/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs b/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs index b8dfef6826..16de637050 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs @@ -23,10 +23,7 @@ public class WidgetHostingService : IWidgetHostingService private const int MaxAttempts = 3; - /// - /// Get the list of current widgets from the WidgetService. - /// - /// A list of widgets, or null if there were no widgets or the list could not be retrieved. + /// public async Task GetWidgetsAsync() { var attempt = 0; @@ -35,7 +32,7 @@ public async Task GetWidgetsAsync() try { _widgetHost ??= await Task.Run(() => WidgetHost.Register(new WidgetHostContext("BAA93438-9B07-4554-AD09-7ACCD7D4F031"))); - return await Task.Run(() => _widgetHost.GetWidgets()); + return await Task.Run(() => _widgetHost.GetWidgets()) ?? []; } catch (COMException ex) when (ex.HResult == RpcServerUnavailable || ex.HResult == RpcCallFailed) { @@ -52,11 +49,10 @@ public async Task GetWidgetsAsync() } } - return null; + return []; } - /// Gets the widget with the given ID. - /// The widget, or null if one could not be retrieved. + /// public async Task GetWidgetAsync(string widgetId) { var attempt = 0; @@ -83,10 +79,7 @@ public async Task GetWidgetAsync(string widgetId) return null; } - /// - /// Create and return a new widget. - /// - /// The new widget, or null if one could not be created. + /// public async Task CreateWidgetAsync(string widgetDefinitionId, WidgetSize widgetSize) { var attempt = 0; @@ -113,10 +106,7 @@ public async Task CreateWidgetAsync(string widgetDefinitionId, WidgetSiz return null; } - /// - /// Get the catalog of widgets from the WidgetService. - /// - /// The catalog of widgets, or null if one could not be created. + /// public async Task GetWidgetCatalogAsync() { var attempt = 0; @@ -149,11 +139,7 @@ public async Task GetWidgetCatalogAsync() return _widgetCatalog; } - /// - /// Get the list of WidgetProviderDefinitions from the WidgetService. - /// - /// A list of WidgetProviderDefinitions, or an empty list if there were no widgets - /// or the list could not be retrieved. + /// public async Task GetProviderDefinitionsAsync() { var attempt = 0; @@ -182,11 +168,7 @@ public async Task GetProviderDefinitionsAsync() return []; } - /// - /// Get the list of WidgetDefinitions from the WidgetService. - /// - /// A list of WidgetDefinitions, or an empty list if there were no widgets - /// or the list could not be retrieved. + /// public async Task GetWidgetDefinitionsAsync() { var attempt = 0; @@ -215,11 +197,7 @@ public async Task GetWidgetDefinitionsAsync() return []; } - /// - /// Get the WidgetDefinition for the given WidgetDefinitionId from the WidgetService. - /// - /// The WidgetDefinition, or null if the widget definition could not be found - /// or there was an error retrieving it. + /// public async Task GetWidgetDefinitionAsync(string widgetDefinitionId) { var attempt = 0; diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs index 817b63fed7..b5cd2f647a 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs @@ -217,15 +217,14 @@ private async Task InitializeDashboard() private async Task InitializePinnedWidgetListAsync(bool isFirstDashboardRun) { var hostWidgets = await GetPreviouslyPinnedWidgets(); - - if ((hostWidgets == null || hostWidgets.Length == 0) && isFirstDashboardRun) + if ((hostWidgets.Length == 0) && isFirstDashboardRun) { // If it's the first time the Dashboard has been displayed and we have no other widgets pinned to a // different version of Dev Home, pin some default widgets. _log.Information($"Pin default widgets"); await PinDefaultWidgetsAsync(); } - else if (hostWidgets != null) + else { await RestorePinnedWidgetsAsync(hostWidgets); } @@ -235,11 +234,10 @@ private async Task GetPreviouslyPinnedWidgets() { _log.Information("Get widgets for current host"); var unsafeHostWidgets = await ViewModel.WidgetHostingService.GetWidgetsAsync(); - - if (unsafeHostWidgets == null) + if (unsafeHostWidgets.Length == 0) { _log.Information($"Found 0 widgets for this host"); - return null; + return []; } var comSafeHostWidgets = new List(); @@ -380,7 +378,7 @@ private async Task DeleteAbandonedWidgetAsync(ComSafeWidget widget) await widget.DeleteAsync(); var newWidgetList = await ViewModel.WidgetHostingService.GetWidgetsAsync(); - length = (newWidgetList == null) ? 0 : newWidgetList.Length; + length = newWidgetList.Length; _log.Information($"After delete, {length} widgets for this host"); } From a0ace42fa4d1332cfc9c85ec6c424079fb79ed83 Mon Sep 17 00:00:00 2001 From: Soham Das <93530301+SohamDas2021@users.noreply.github.com> Date: Thu, 18 Apr 2024 13:44:50 -0700 Subject: [PATCH 118/121] Fix env var data on Dev Drive insights (#2690) --- .../Strings/en-us/Resources.resw | 4 ++++ .../DevDriveOptimizerCardViewModel.cs | 14 ++++++++++++-- .../ViewModels/DevDriveInsightsViewModel.cs | 10 +++++----- .../Views/DevDriveInsightsPage.xaml | 2 +- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw index ce4d1ddcbd..893ebbe7a1 100644 --- a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw +++ b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw @@ -201,6 +201,10 @@ Global environment variable {0} is set to {1}, which is not located on Dev Drive. Move contents of this folder to a directory on Dev Drive such as {2} and set {3} to that chosen directory on Dev Drive Optimizer dev drive description
+ + Move contents of {0} to a directory on Dev Drive such as {1} and set {2} to that chosen directory on Dev Drive + Optimizer dev drive description when environment variable is not set + Search The placholder text for the settings auto suggest box diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs index e0175b81de..c98c7d0cf1 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs @@ -62,7 +62,8 @@ public DevDriveOptimizerCardViewModel( string existingCacheLocation, string exampleLocationOnDevDrive, string environmentVariableToBeSet, - List existingDevDriveLetters) + List existingDevDriveLetters, + bool environmentVariableHasValue) { OptimizeDevDriveDialogViewModelFactory = optimizeDevDriveDialogViewModelFactory; ExistingDevDriveLetters = existingDevDriveLetters; @@ -71,7 +72,16 @@ public DevDriveOptimizerCardViewModel( ExampleLocationOnDevDrive = exampleLocationOnDevDrive; EnvironmentVariableToBeSet = environmentVariableToBeSet; var stringResource = new StringResource("DevHome.Customization.pri", "DevHome.Customization/Resources"); - OptimizerDevDriveDescription = stringResource.GetLocalized("OptimizerDevDriveDescription", EnvironmentVariableToBeSet, ExistingCacheLocation, ExampleLocationOnDevDrive, EnvironmentVariableToBeSet); + + if (environmentVariableHasValue) + { + OptimizerDevDriveDescription = stringResource.GetLocalized("OptimizerDevDriveDescription", EnvironmentVariableToBeSet, ExistingCacheLocation, ExampleLocationOnDevDrive, EnvironmentVariableToBeSet); + } + else + { + OptimizerDevDriveDescription = stringResource.GetLocalized("OptimizerDevDriveDescriptionWithEnvVarNotSet", ExistingCacheLocation, ExampleLocationOnDevDrive, EnvironmentVariableToBeSet); + } + MakeTheChangeText = stringResource.GetLocalized("MakeTheChangeText"); DevDriveOptimizationSuggestion = stringResource.GetLocalized("DevDriveOptimizationSuggestion"); } diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs index 15000d4f2c..31136911b7 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs @@ -330,12 +330,11 @@ public void UpdateListViewModelList() var matchingSubdirectory = subDirectories.FirstOrDefault(subdir => subdir.StartsWith(cacheDirectory, StringComparison.OrdinalIgnoreCase)); if (Directory.Exists(matchingSubdirectory)) { - if (matchingSubdirectory.Contains("PythonSoftwareFoundation")) + var actualCacheDirectory = Path.Join(matchingSubdirectory, "LocalCache", "Local", "pip", CacheStr); + if (matchingSubdirectory.Contains("PythonSoftwareFoundation") && Directory.Exists(actualCacheDirectory)) { - return Path.Join(matchingSubdirectory, "LocalCache", "Local", "pip", CacheStr); + return actualCacheDirectory; } - - return matchingSubdirectory; } } } @@ -381,7 +380,8 @@ public void UpdateOptimizerListViewModelList() existingCacheLocation, exampleDirectory!, // example location on dev drive to move cache to cache.EnvironmentVariable!, // environmentVariableToBeSet - existingDevDriveLetters); + existingDevDriveLetters, + !string.IsNullOrEmpty(environmentVariablePath)); DevDriveOptimizerCardCollection.Add(card); } diff --git a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml index e9ad085133..ae826191d3 100644 --- a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml +++ b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml @@ -10,6 +10,6 @@ - +
From 18373685f755afaf60c17b5970d63ed8a2f2a6ad Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:04:53 -0700 Subject: [PATCH 119/121] String tweaks for machine configuration and windows customization (#2688) --- .../DevHome.Customization/Strings/en-us/Resources.resw | 6 +++--- .../DevHome.SetupFlow/Strings/en-us/Resources.resw | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw index 893ebbe7a1..a79239d84b 100644 --- a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw +++ b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw @@ -126,15 +126,15 @@ Dev drive size free - All things, Dev Drives, optimizations, etc. + All things Dev Drive, optimizations, etc. The description for the Dev Drive Insights settings card - Dev Drive Insights + Dev Drive insights The header for the Dev Drive Insights settings card - Dev Drive Insights + Dev Drive insights Header for Dev Drive insights page in the breadcrumb bar diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw index 804ff744f0..e8d2b9a8cd 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -222,8 +222,8 @@ {Locked="{0}"}Title for the page showing the contents of a configuration file. {0} is replaced by the configuration file name. - Winget configuration is currently disabled on your machine. We are initiating the enablement process, which may take a few minutes. Please try again soon. - Message displayed to the user when they attempt to activate Dev Home from a file but the Winget configuration feature is disabled. + WinGet configuration is currently disabled on your machine. We are initiating the enablement process, which may take a few minutes. Please try again soon. + Message displayed to the user when they attempt to activate Dev Home from a file but the WinGet configuration feature is disabled. The DSC flow cannot be activated while machine configuration is in progress. Please complete your current configuration tasks and then try again. @@ -526,7 +526,7 @@ Header text for a group of controls giving multiple choices for configuring the machine, but not a full setup flow - Clone repositories, install applications, and generate Winget Configuration files together + Clone repositories, install applications, and generate WinGet Configuration files together. Body text description for a card than when clicked takes the user to a multi-step flow for setting up their machine @@ -538,7 +538,7 @@ Header for a card that when clicked takes the user to a multi-step flow for setting up for a remote machine - Clone repositories, install applications, and generate a Winget Configuration file to apply to an environment. + Clone repositories, install applications, and generate a WinGet Configuration file to apply to an environment. Body text description for a card than when clicked takes the user to a multi-step flow for setting up their machine @@ -1573,7 +1573,7 @@ Text for when the configuration operation on a remote machine is on going but the extension is waiting for the current user to logon to the machine - Winget skipped this unit + WinGet skipped this unit Text for when the a part of the configuration on the remove machine was completed but its completed status was set to skipped. From 14074f6d3202761e0879d2495b8a791b0c6005b8 Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:55:27 -0700 Subject: [PATCH 120/121] Use a SettingsCard instead of a SettingsExpander for Dev Drive insights (#2692) --- .../Views/DevDriveInsightsView.xaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsView.xaml b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsView.xaml index 372fea5916..9091140efa 100644 --- a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsView.xaml +++ b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsView.xaml @@ -24,10 +24,10 @@ - - + + - + @@ -39,15 +39,15 @@
- + - + - + From ccc9498e657215b58d424cf58b81940ff21977f1 Mon Sep 17 00:00:00 2001 From: Jeff Whiteside Date: Thu, 18 Apr 2024 15:40:58 -0700 Subject: [PATCH 121/121] Change Quiet background processes icon to an existing icon (#2693) --- .../Views/QuietBackgroundProcessesView.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesView.xaml b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesView.xaml index b92dfcbbe6..8362045622 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesView.xaml +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesView.xaml @@ -12,7 +12,7 @@ - +