From 18e772b5d994bd3924c4e54ecea4ae4b62eeba93 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 11 Jul 2024 09:51:18 -0700 Subject: [PATCH 01/82] [Environments] Changed UI to match Setup flow (#3347) * Added details for narrator * Removed alternative sorting * Removed whitespace * Added provider-wise classification * Added sorting * Fixed filtering * Changed default search * Changed to compare by ID * Removed extra strings * Added more localization * Ordering variables * NIT change * Remove space Co-authored-by: Branden Bonaby <105318831+bbonaby@users.noreply.github.com> * Fixed extra space while filtering * Added Enum, Changed swapping --------- Co-authored-by: Huzaifa Danish Co-authored-by: Branden Bonaby <105318831+bbonaby@users.noreply.github.com> --- .../Selectors/CardItemTemplateSelector.cs | 4 +- .../Strings/en-us/Resources.resw | 8 - .../ViewModels/ComputeSystemCardBase.cs | 18 +- .../ViewModels/LandingPageViewModel.cs | 206 ++++++++++-------- .../ViewModels/PerProviderViewModel.cs | 144 ++++++++++++ .../Views/LandingPage.xaml | 74 +++++-- .../ComputeSystemCardViewModel.cs | 10 +- 7 files changed, 339 insertions(+), 125 deletions(-) create mode 100644 tools/Environments/DevHome.Environments/ViewModels/PerProviderViewModel.cs diff --git a/tools/Environments/DevHome.Environments/Selectors/CardItemTemplateSelector.cs b/tools/Environments/DevHome.Environments/Selectors/CardItemTemplateSelector.cs index 6637ba782f..bafd2ea6c4 100644 --- a/tools/Environments/DevHome.Environments/Selectors/CardItemTemplateSelector.cs +++ b/tools/Environments/DevHome.Environments/Selectors/CardItemTemplateSelector.cs @@ -11,7 +11,7 @@ public class CardItemTemplateSelector : DataTemplateSelector { public DataTemplate? CreateComputeSystemOperationTemplate { get; set; } - public DataTemplate? ComputeSystemTemplate { get; set; } + public DataTemplate? PerProviderComputeSystemTemplate { get; set; } protected override DataTemplate? SelectTemplateCore(object item) { @@ -35,7 +35,7 @@ public class CardItemTemplateSelector : DataTemplateSelector } else { - return ComputeSystemTemplate; + return PerProviderComputeSystemTemplate; } } } diff --git a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw index 81f8abbe83..01972f5c96 100644 --- a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw +++ b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw @@ -173,10 +173,6 @@ Name: Ascending Text for sorting in ascending order of name - - Secondary Name: Ascending - Text for sorting by secondary name - Sort: Text labeling sort by text block @@ -225,10 +221,6 @@ Name: Descending Text for sorting in descending order of name - - Secondary Name: Descending - Text for sorting by secondary name in descending order - Last Connected Text for sorting by last connected time diff --git a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs index 0852a52ce4..a156405a9a 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs @@ -3,6 +3,7 @@ using System; using System.Collections.ObjectModel; +using System.Text; using CommunityToolkit.Mvvm.ComponentModel; using DevHome.Common.Environments.Models; using DevHome.Common.Services; @@ -59,18 +60,29 @@ public abstract partial class ComputeSystemCardBase : ObservableObject public string ComputeSystemId { get; protected set; } = string.Empty; + private readonly StringResource _stringResource = new("DevHome.Environments.pri", "DevHome.Environments/Resources"); + + private readonly StringResource _stringResourceCommon = new("DevHome.Common.pri", "DevHome.Common/Resources"); + [ObservableProperty] private string _uiMessageToDisplay = string.Empty; public ComputeSystemCardBase() { - var stringResource = new StringResource("DevHome.Environments.pri", "DevHome.Environments/Resources"); - _moreOptionsButtonName = stringResource.GetLocalized("MoreOptionsButtonName"); + _moreOptionsButtonName = _stringResource.GetLocalized("MoreOptionsButtonName"); } public override string ToString() { - return $"{Name} {AlternativeName}"; + var description = new StringBuilder(Name); + + if (!string.IsNullOrEmpty(AlternativeName)) + { + description.Append(AlternativeName); + } + + description.Append(_stringResourceCommon.GetLocalized($"ComputeSystem{State}")); + return description.ToString(); } /// diff --git a/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs index 3b6cf80e45..5fa8022064 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs @@ -51,9 +51,7 @@ public partial class LandingPageViewModel : ObservableObject, IDisposable public bool IsLoading { get; set; } - public ObservableCollection ComputeSystemCards { get; set; } = new(); - - public AdvancedCollectionView ComputeSystemCardsView { get; set; } + public ObservableCollection PerProviderViewModels { get; set; } = new(); public bool HasPageLoadedForTheFirstTime { get; set; } @@ -81,7 +79,14 @@ public partial class LandingPageViewModel : ObservableObject, IDisposable [ObservableProperty] private bool _shouldShowCreationHeader; - private const int SortUnselected = -1; + private enum SortOptions + { + Alphabetical, + AlphabeticalDescending, + LastConnected, + } + + private const int DefaultSortIndex = (int)SortOptions.LastConnected; public ObservableCollection Providers { get; set; } @@ -100,12 +105,9 @@ public LandingPageViewModel( _stringResource = new StringResource("DevHome.Environments.pri", "DevHome.Environments/Resources"); - SelectedSortIndex = SortUnselected; + SelectedSortIndex = DefaultSortIndex; Providers = new() { _stringResource.GetLocalized("AllProviders") }; _lastSyncTime = _stringResource.GetLocalized("MomentsAgo"); - - ComputeSystemCardsView = new AdvancedCollectionView(ComputeSystemCards); - ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("IsCardCreating", SortDirection.Descending)); } public void Initialize(StackedNotificationsBehavior notificationQueue) @@ -117,7 +119,7 @@ public void Initialize(StackedNotificationsBehavior notificationQueue) public async Task SyncButton() { // Reset the sort and filter - SelectedSortIndex = SortUnselected; + SelectedSortIndex = DefaultSortIndex; Providers = new ObservableCollection { _stringResource.GetLocalized("AllProviders") }; SelectedProviderIndex = 0; _wasSyncButtonClicked = true; @@ -226,19 +228,24 @@ public async Task LoadModelAsync(bool useDebugValues = false) await RunSyncTimmer(); }); - lock (ComputeSystemCards) + foreach (var providerViewModel in PerProviderViewModels) { - for (var i = ComputeSystemCards.Count - 1; i >= 0; i--) + var computeSystemCards = providerViewModel.ComputeSystems; + lock (computeSystemCards) { - if (ComputeSystemCards[i] is ComputeSystemViewModel computeSystemViewModel) + for (var i = computeSystemCards.Count - 1; i >= 0; i--) { - computeSystemViewModel.RemoveStateChangedHandler(); - ComputeSystemCards[i].ComputeSystemErrorReceived -= OnComputeSystemOperationError; - ComputeSystemCards.RemoveAt(i); + if (computeSystemCards[i] is ComputeSystemViewModel computeSystemViewModel) + { + computeSystemViewModel.RemoveStateChangedHandler(); + computeSystemCards[i].ComputeSystemErrorReceived -= OnComputeSystemOperationError; + computeSystemCards.RemoveAt(i); + } } } } + PerProviderViewModels.Clear(); _notificationsHelper?.ClearNotifications(); CallToActionText = null; CallToActionHyperLinkButtonText = null; @@ -261,18 +268,26 @@ public async Task LoadModelAsync(bool useDebugValues = false) private void SetupCreateComputeSystemOperationForUI() { // Remove all the operations from view and then add the ones the manager has. - _log.Information($"Adding any new create compute system operations to ComputeSystemCards list"); + _log.Information($"Adding any new create compute system operations to computeSystemCards list"); var curOperations = _computeSystemManager.GetRunningOperationsForCreation(); - lock (ComputeSystemCards) + var providerViewModel = PerProviderViewModels.FirstOrDefault(provider => provider.ProviderID.Equals(_computeSystemManager.ComputeSystemSetupItem?.AssociatedProvider.Id, StringComparison.OrdinalIgnoreCase)); + + if (providerViewModel == null) + { + return; + } + + var computeSystemCards = providerViewModel.ComputeSystems; + lock (computeSystemCards) { - for (var i = ComputeSystemCards.Count - 1; i >= 0; i--) + for (var i = computeSystemCards.Count - 1; i >= 0; i--) { - if (ComputeSystemCards[i] is CreateComputeSystemOperationViewModel operationViewModel) + if (computeSystemCards[i] is CreateComputeSystemOperationViewModel operationViewModel) { operationViewModel!.RemoveEventHandlers(); operationViewModel.ComputeSystemErrorReceived -= OnComputeSystemOperationError; - ComputeSystemCards.RemoveAt(i); + computeSystemCards.RemoveAt(i); } } @@ -289,11 +304,11 @@ private void SetupCreateComputeSystemOperationForUI() operation); operationViewModel.ComputeSystemErrorReceived += OnComputeSystemOperationError; - ComputeSystemCards.Insert(0, operationViewModel); + computeSystemCards.Insert(0, operationViewModel); _log.Information($"Found new create compute system operation for provider {operation.ProviderDetails.ComputeSystemProvider}, with name {operation.EnvironmentName}"); } - ComputeSystemCardsView.Refresh(); + providerViewModel.ComputeSystemAdvancedView.Refresh(); UpdateCallToActionText(); } } @@ -302,8 +317,8 @@ private async Task AddAllComputeSystemsFromAProvider(ComputeSystemsLoadedData da { _notificationsHelper?.DisplayComputeSystemEnumerationErrors(data); var provider = data.ProviderDetails.ComputeSystemProvider; - var computeSystemList = data.DevIdToComputeSystemMap.Values.SelectMany(x => x.ComputeSystems).ToList() ?? []; + var loginId = data.DevIdToComputeSystemMap?.Keys.FirstOrDefault()?.DeveloperId.LoginId; // In the future when we support switching between accounts in the environments page, we will need to handle this differently. // for now we'll show all the compute systems from a provider. @@ -339,14 +354,14 @@ await _mainWindow.DispatcherQueue.EnqueueAsync(() => try { Providers.Add(provider.DisplayName); + List tempComputeSystemViewModels = new(); foreach (var computeSystemViewModel in computeSystemViewModels) { computeSystemViewModel.InitializeUXData(); - lock (ComputeSystemCards) - { - ComputeSystemCards.Add(computeSystemViewModel); - } + tempComputeSystemViewModels.Add(computeSystemViewModel); } + + PerProviderViewModels.Add(new PerProviderViewModel(provider.DisplayName, provider.Id, loginId ?? string.Empty, tempComputeSystemViewModels, _mainWindow)); } catch (Exception ex) { @@ -363,22 +378,32 @@ await _mainWindow.DispatcherQueue.EnqueueAsync(() => [RelayCommand] public void SearchHandler(string query) { - ComputeSystemCardsView.Filter = system => + var currentProviders = PerProviderViewModels.ToList(); + PerProviderViewModels.Clear(); + + var dontShowIndices = new List(); + for (var i = 0; i < currentProviders.Count; i++) { - if (system is CreateComputeSystemOperationViewModel createComputeSystemOperationViewModel) + var providerViewModel = currentProviders[i]; + providerViewModel.SearchHandler(query); + + if (!providerViewModel.IsVisible) { - return createComputeSystemOperationViewModel.EnvironmentName.Contains(query, StringComparison.OrdinalIgnoreCase); + dontShowIndices.Add(i); } - - if (system is ComputeSystemViewModel computeSystemViewModel) + else { - var systemName = computeSystemViewModel.ComputeSystem!.DisplayName.Value; - var systemAltName = computeSystemViewModel.ComputeSystem.SupplementalDisplayName.Value; - return systemName.Contains(query, StringComparison.OrdinalIgnoreCase) || systemAltName.Contains(query, StringComparison.OrdinalIgnoreCase); + PerProviderViewModels.Add(providerViewModel); } + } - return false; - }; + // Move all don't show indices to the end of the list + // so that the visible providers are only shown. + // Add all the hidden providers + foreach (var index in dontShowIndices) + { + PerProviderViewModels.Add(currentProviders[index]); + } } /// @@ -387,27 +412,31 @@ public void SearchHandler(string query) [RelayCommand] public void ProviderHandler(int selectedIndex) { - SelectedProviderIndex = selectedIndex; - var currentProvider = Providers[SelectedProviderIndex]; - ComputeSystemCardsView.Filter = system => + // Swap positions of the selected provider and the first provider in the list + // so that the selected provider is always at the top of the list and no extra + // UI space is shown. + if (selectedIndex != 0) { - if (currentProvider.Equals(_stringResource.GetLocalized("AllProviders"), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - if (system is CreateComputeSystemOperationViewModel createComputeSystemOperationViewModel) + var actualIndex = -1; + for (var i = 0; i < PerProviderViewModels.Count; i++) { - return createComputeSystemOperationViewModel.ProviderDisplayName.Equals(currentProvider, StringComparison.OrdinalIgnoreCase); + if (PerProviderViewModels[i].ProviderName.Equals(Providers[selectedIndex], StringComparison.OrdinalIgnoreCase)) + { + actualIndex = i; + break; + } } - if (system is ComputeSystemViewModel computeSystemViewModel) - { - return computeSystemViewModel.ProviderDisplayName.Equals(currentProvider, StringComparison.OrdinalIgnoreCase); - } + var temp = PerProviderViewModels[0]; + PerProviderViewModels[0] = PerProviderViewModels[actualIndex]; + PerProviderViewModels[actualIndex] = temp; + } - return false; - }; + var currentProvider = Providers[selectedIndex]; + foreach (var providerViewModel in PerProviderViewModels) + { + providerViewModel.ProviderHandler(currentProvider); + } } /// @@ -419,30 +448,9 @@ public void ProviderHandler(int selectedIndex) [RelayCommand] public void SortHandler() { - ComputeSystemCardsView.SortDescriptions.Clear(); - - if (SelectedSortIndex == SortUnselected) - { - ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("IsCardCreating", SortDirection.Descending)); - } - - switch (SelectedSortIndex) + foreach (var providerViewModel in PerProviderViewModels) { - case 0: - ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("Name", SortDirection.Ascending)); - break; - case 1: - ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("Name", SortDirection.Descending)); - break; - case 2: - ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("AlternativeName", SortDirection.Ascending)); - break; - case 3: - ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("AlternativeName", SortDirection.Descending)); - break; - case 4: - ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("LastConnected", SortDirection.Ascending)); - break; + providerViewModel.SortHandler(SelectedSortIndex); } } @@ -456,32 +464,45 @@ private void AddNewlyCreatedComputeSystem(ComputeSystemViewModel computeSystemVi } ComputeSystemCardBase? viewModel = default; - lock (ComputeSystemCards) + var providerViewModel = PerProviderViewModels.FirstOrDefault(provider => provider.ProviderID.Equals(computeSystemViewModel.AssociatedProviderId, StringComparison.OrdinalIgnoreCase)); + if (providerViewModel != null) { - viewModel = ComputeSystemCards.FirstOrDefault(viewBase => viewBase.ComputeSystemId.Equals(computeSystemViewModel.ComputeSystemId, StringComparison.OrdinalIgnoreCase)); - } + var computeSystemCards = providerViewModel.ComputeSystems; + lock (computeSystemCards) + { + viewModel = computeSystemCards.FirstOrDefault(viewBase => viewBase.ComputeSystemId.Equals(computeSystemViewModel.ComputeSystemId, StringComparison.OrdinalIgnoreCase)); + } - if (viewModel == null) - { - _mainWindow.DispatcherQueue.EnqueueAsync(() => + if (viewModel == null) { - lock (ComputeSystemCards) + _mainWindow.DispatcherQueue.EnqueueAsync(() => { - computeSystemViewModel.ComputeSystemErrorReceived += OnComputeSystemOperationError; - ComputeSystemCards.Insert(0, computeSystemViewModel); - } - - ComputeSystemCardsView.Refresh(); - }); + lock (computeSystemCards) + { + computeSystemViewModel.ComputeSystemErrorReceived += OnComputeSystemOperationError; + computeSystemCards.Insert(0, computeSystemViewModel); + } + + providerViewModel.ComputeSystemAdvancedView.Refresh(); + }); + } } }); } private bool RemoveComputeSystemCard(ComputeSystemCardBase computeSystemCard) { - lock (ComputeSystemCards) + var providerViewModel = PerProviderViewModels.FirstOrDefault(provider => provider.ProviderID.Equals(computeSystemCard.AssociatedProviderId, StringComparison.OrdinalIgnoreCase)); + + if (providerViewModel == null) + { + return false; + } + + var computeSystemCards = providerViewModel.ComputeSystems; + lock (computeSystemCards) { - return ComputeSystemCards.Remove(computeSystemCard); + return computeSystemCards.Remove(computeSystemCard); } } @@ -516,7 +537,8 @@ public void Dispose() private void UpdateCallToActionText() { // if there are cards in the UI don't update the text and keep their values as null. - if (ComputeSystemCards.Count > 0) + // Check if there are any compute systems in PerProviderViewModels + if (PerProviderViewModels.Any(provider => provider.ComputeSystems.Count > 0)) { CallToActionText = null; return; diff --git a/tools/Environments/DevHome.Environments/ViewModels/PerProviderViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/PerProviderViewModel.cs new file mode 100644 index 0000000000..d0a8a5a455 --- /dev/null +++ b/tools/Environments/DevHome.Environments/ViewModels/PerProviderViewModel.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.WinUI; +using CommunityToolkit.WinUI.Collections; +using DevHome.Common.Services; +using Microsoft.UI.Xaml; + +namespace DevHome.Environments.ViewModels; + +// View model representing a compute system provider and its associated compute systems. +public partial class PerProviderViewModel : ObservableObject +{ + public string ProviderName { get; } + + public string ProviderID { get; } + + public string DecoratedDevID { get; } + + public ObservableCollection ComputeSystems { get; } + + public AdvancedCollectionView ComputeSystemAdvancedView { get; set; } + + private readonly StringResource _stringResource; + + private readonly Window _mainWindow; + + [ObservableProperty] + private bool _isVisible = true; + + public PerProviderViewModel(string name, string id, string associatedDevID, List computeSystems, Window mainWindow) + { + ProviderName = name; + ProviderID = id; + DecoratedDevID = associatedDevID.Length > 0 ? '(' + associatedDevID + ')' : string.Empty; + ComputeSystems = new ObservableCollection(computeSystems); + _mainWindow = mainWindow; + + _stringResource = new StringResource("DevHome.Environments.pri", "DevHome.Environments/Resources"); + ComputeSystemAdvancedView = new AdvancedCollectionView(ComputeSystems); + ComputeSystemAdvancedView.SortDescriptions.Add(new SortDescription("IsCardCreating", SortDirection.Descending)); + } + + /// + /// Updates the view model to show only the compute systems that match the search criteria. + /// + public void SearchHandler(string query) + { + ComputeSystemAdvancedView.Filter = system => + { + if (system is CreateComputeSystemOperationViewModel createComputeSystemOperationViewModel) + { + return createComputeSystemOperationViewModel.EnvironmentName.Contains(query, StringComparison.OrdinalIgnoreCase); + } + + if (system is ComputeSystemViewModel computeSystemViewModel) + { + var systemName = computeSystemViewModel.ComputeSystem!.DisplayName.Value; + var systemAltName = computeSystemViewModel.ComputeSystem.SupplementalDisplayName.Value; + return systemName.Contains(query, StringComparison.OrdinalIgnoreCase) || systemAltName.Contains(query, StringComparison.OrdinalIgnoreCase); + } + + return false; + }; + + _mainWindow.DispatcherQueue.EnqueueAsync(() => + { + IsVisible = ComputeSystemAdvancedView.Count > 0; + }); + } + + /// + /// Updates the view model to sort the compute systems according to the sort criteria. + /// + /// + /// New SortDescription property names should be added as new properties to + /// + public void SortHandler(int selectedSortIndex) + { + ComputeSystemAdvancedView.SortDescriptions.Clear(); + switch (selectedSortIndex) + { + case 0: + ComputeSystemAdvancedView.SortDescriptions.Add(new SortDescription("Name", SortDirection.Ascending)); + break; + case 1: + ComputeSystemAdvancedView.SortDescriptions.Add(new SortDescription("Name", SortDirection.Descending)); + break; + case 2: + ComputeSystemAdvancedView.SortDescriptions.Add(new SortDescription("LastConnected", SortDirection.Ascending)); + break; + } + } + + /// + /// Updates the view model to filter the compute systems according to the provider. + /// + public void ProviderHandler(string currentProvider) + { + ComputeSystemAdvancedView.Filter = system => + { + if (currentProvider.Equals(_stringResource.GetLocalized("AllProviders"), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (system is CreateComputeSystemOperationViewModel createComputeSystemOperationViewModel) + { + return createComputeSystemOperationViewModel.ProviderDisplayName.Equals(currentProvider, StringComparison.OrdinalIgnoreCase); + } + + if (system is ComputeSystemViewModel computeSystemViewModel) + { + return computeSystemViewModel.ProviderDisplayName.Equals(currentProvider, StringComparison.OrdinalIgnoreCase); + } + + return false; + }; + + _mainWindow.DispatcherQueue.EnqueueAsync(() => + { + IsVisible = ComputeSystemAdvancedView.Count > 0; + }); + } + + public override string ToString() + { + StringBuilder description = new(ProviderName); + + if (!string.IsNullOrEmpty(DecoratedDevID)) + { + description.Append(DecoratedDevID); + } + + return description.ToString(); + } +} diff --git a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml index 1fb634c345..77fdad883f 100644 --- a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml +++ b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml @@ -74,7 +74,7 @@ - + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/DevHome.PI.csproj b/tools/PI/DevHome.PI/DevHome.PI.csproj index 71c261c81a..2f255067cc 100644 --- a/tools/PI/DevHome.PI/DevHome.PI.csproj +++ b/tools/PI/DevHome.PI/DevHome.PI.csproj @@ -45,7 +45,7 @@ - + @@ -147,7 +147,7 @@ MSBuild:Compile - + MSBuild:Compile diff --git a/tools/PI/DevHome.PI/Helpers/WatsonHelper.cs b/tools/PI/DevHome.PI/Helpers/WatsonHelper.cs deleted file mode 100644 index 71f45d07a0..0000000000 --- a/tools/PI/DevHome.PI/Helpers/WatsonHelper.cs +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Diagnostics.Eventing.Reader; -using System.Globalization; -using System.IO; -using System.Linq; -using DevHome.PI.Models; - -namespace DevHome.PI.Helpers; - -internal sealed class WatsonHelper : IDisposable -{ - private const string WatsonQueryPart1 = "(*[System[Provider[@Name=\"Application Error\"]]] and *[System[EventID=1000]])"; - private const string WatsonQueryPart2 = "(*[System[Provider[@Name=\"Windows Error Reporting\"]]] and *[System[EventID=1001]])"; - - private readonly Process targetProcess; - private readonly EventLogWatcher? eventLogWatcher; - private readonly ObservableCollection? watsonOutput; - private readonly ObservableCollection? winLogsPageOutput; - - public WatsonHelper(Process targetProcess, ObservableCollection? watsonOutput, ObservableCollection? winLogsPageOutput) - { - this.targetProcess = targetProcess; - this.targetProcess.Exited += TargetProcess_Exited; - this.watsonOutput = watsonOutput; - this.winLogsPageOutput = winLogsPageOutput; - - try - { - // Subscribe for Application events matching the processName. - var filterQuery = string.Format(CultureInfo.CurrentCulture, "{0} or {1}", WatsonQueryPart1, WatsonQueryPart2); - EventLogQuery subscriptionQuery = new("Application", PathType.LogName, filterQuery); - eventLogWatcher = new EventLogWatcher(subscriptionQuery); - eventLogWatcher.EventRecordWritten += new EventHandler(EventLogEventRead); - } - catch (EventLogReadingException) - { - var message = CommonHelper.GetLocalizedString("WatsonStartErrorMessage"); - WinLogsEntry entry = new(DateTime.Now, WinLogCategory.Error, message, WinLogsHelper.WatsonName); - winLogsPageOutput?.Add(entry); - } - } - - public void Start() - { - if (eventLogWatcher is not null) - { - eventLogWatcher.Enabled = true; - } - } - - public void Stop() - { - if (eventLogWatcher is not null) - { - eventLogWatcher.Enabled = false; - } - } - - public void Dispose() - { - if (eventLogWatcher is not null) - { - eventLogWatcher.Dispose(); - } - - GC.SuppressFinalize(this); - } - - public void EventLogEventRead(object? obj, EventRecordWrittenEventArgs eventArg) - { - var eventRecord = eventArg.EventRecord; - if (eventRecord != null) - { - if (eventRecord.Id == 1000 && eventRecord.ProviderName.Equals("Application Error", StringComparison.OrdinalIgnoreCase)) - { - var filePath = eventRecord.Properties[10].Value.ToString() ?? string.Empty; - if (filePath.Contains(targetProcess.ProcessName, StringComparison.OrdinalIgnoreCase)) - { - var timeGenerated = eventRecord.TimeCreated ?? DateTime.Now; - var moduleName = eventRecord.Properties[3].Value.ToString() ?? string.Empty; - var executable = eventRecord.Properties[0].Value.ToString() ?? string.Empty; - var eventGuid = eventRecord.Properties[12].Value.ToString() ?? string.Empty; - var report = new WatsonReport(timeGenerated, moduleName, executable, eventGuid); - watsonOutput?.Add(report); - - WinLogsEntry entry = new(timeGenerated, WinLogCategory.Error, eventRecord.FormatDescription(), WinLogsHelper.WatsonName); - winLogsPageOutput?.Add(entry); - } - } - else if (eventRecord.Id == 1001 && eventRecord.ProviderName.Equals("Windows Error Reporting", StringComparison.OrdinalIgnoreCase)) - { - // See if we've already put this into our Collection. - for (var i = 0; i < watsonOutput?.Count; i++) - { - var existingReport = watsonOutput[i]; - if (existingReport.EventGuid.Equals(eventRecord.Properties[19].Value.ToString(), StringComparison.OrdinalIgnoreCase)) - { - existingReport.WatsonLog = eventRecord.FormatDescription(); - try - { - // List files available in the archive. - var directoryPath = eventRecord.Properties[16].Value.ToString(); - if (Directory.Exists(directoryPath)) - { - IEnumerable files = Directory.EnumerateFiles(directoryPath); - foreach (var file in files) - { - existingReport.WatsonReportFile = File.ReadAllText(file); - } - } - } - catch - { - } - - break; - } - } - } - } - } - - public List GetWatsonReports() - { - Dictionary reports = []; - EventLog eventLog = new("Application"); - var targetProcessName = targetProcess.ProcessName; - - foreach (EventLogEntry entry in eventLog.Entries) - { - if (entry.InstanceId == 1000 - && entry.Source.Equals("Application Error", StringComparison.OrdinalIgnoreCase) - && entry.ReplacementStrings[10].Contains(targetProcessName, StringComparison.OrdinalIgnoreCase)) - { - var timeGenerated = entry.TimeGenerated; - var moduleName = entry.ReplacementStrings[3]; - var executable = entry.ReplacementStrings[0]; - var eventGuid = entry.ReplacementStrings[12]; - var report = new WatsonReport(timeGenerated, moduleName, executable, eventGuid); - reports.Add(entry.ReplacementStrings[12], report); - } - else if (entry.InstanceId == 1001 - && entry.Source.Equals("Windows Error Reporting", StringComparison.OrdinalIgnoreCase)) - { - // See if we've already put this into our Dictionary. - if (reports.TryGetValue(entry.ReplacementStrings[19], out WatsonReport? report)) - { - report.WatsonLog = entry.Message; - - try - { - // List files available in the archive. - if (Directory.Exists(entry.ReplacementStrings[16])) - { - var files = Directory.EnumerateFiles(entry.ReplacementStrings[16]); - foreach (var file in files) - { - report.WatsonReportFile = File.ReadAllText(file); - } - } - } - catch - { - } - } - } - } - - return reports.Values.ToList(); - } - - private void TargetProcess_Exited(object? sender, EventArgs e) - { - Stop(); - Dispose(); - } -} diff --git a/tools/PI/DevHome.PI/Helpers/WinLogsHelper.cs b/tools/PI/DevHome.PI/Helpers/WinLogsHelper.cs index 9cb7394259..68a80300f2 100644 --- a/tools/PI/DevHome.PI/Helpers/WinLogsHelper.cs +++ b/tools/PI/DevHome.PI/Helpers/WinLogsHelper.cs @@ -3,11 +3,14 @@ using System; using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.Diagnostics; using System.Diagnostics.Eventing.Reader; using System.Threading; +using DevHome.Common.Extensions; using DevHome.PI.Models; using Microsoft.Diagnostics.Tracing; +using Microsoft.UI.Xaml; namespace DevHome.PI.Helpers; @@ -16,19 +19,18 @@ public class WinLogsHelper : IDisposable public const string EtwLogsName = "ETW Logs"; public const string DebugOutputLogsName = "DebugOutput"; public const string EventViewerName = "EventViewer"; - public const string WatsonName = "Watson"; + public const string WERName = "WER"; private readonly ETWHelper etwHelper; private readonly DebugMonitor debugMonitor; private readonly EventViewerHelper eventViewerHelper; - private readonly WatsonHelper watsonHelper; private readonly ObservableCollection output; private readonly Process targetProcess; + private readonly WERHelper _werHelper; private Thread? etwThread; private Thread? debugMonitorThread; private Thread? eventViewerThread; - private Thread? watsonThread; public WinLogsHelper(Process targetProcess, ObservableCollection output) { @@ -44,11 +46,10 @@ public WinLogsHelper(Process targetProcess, ObservableCollection o // Initialize EventViewer eventViewerHelper = new EventViewerHelper(targetProcess, output); - // Initialize Watson - watsonHelper = new WatsonHelper(targetProcess, null, output); + _werHelper = Application.Current.GetService(); } - public void Start(bool isEtwEnabled, bool isDebugOutputEnabled, bool isEventViewerEnabled, bool isWatsonEnabled) + public void Start(bool isEtwEnabled, bool isDebugOutputEnabled, bool isEventViewerEnabled, bool isWEREnabled) { if (isEtwEnabled) { @@ -65,9 +66,9 @@ public void Start(bool isEtwEnabled, bool isDebugOutputEnabled, bool isEventView StartEventViewerThread(); } - if (isWatsonEnabled) + if (isWEREnabled) { - StartWatsonThread(); + ((INotifyCollectionChanged)_werHelper.WERReports).CollectionChanged += WEREvents_CollectionChanged; } } @@ -82,8 +83,8 @@ public void Stop() // Stop Event Viewer StopEventViewerThread(); - // Stop Watson - StopWatsonThread(); + // Stop WER + ((INotifyCollectionChanged)_werHelper.WERReports).CollectionChanged -= WEREvents_CollectionChanged; } public void Dispose() @@ -91,7 +92,6 @@ public void Dispose() etwHelper.Dispose(); debugMonitor.Dispose(); eventViewerHelper.Dispose(); - watsonHelper.Dispose(); GC.SuppressFinalize(this); } @@ -169,28 +169,21 @@ private void StopEventViewerThread() } } - private void StartWatsonThread() + private void WEREvents_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { - // Stop and close existing thread if any - StopWatsonThread(); - - // Start a new thread - watsonThread = new Thread(() => + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null) { - // Start Watson logs - watsonHelper.Start(); - }); - watsonThread.Name = WatsonName + " Thread"; - watsonThread.Start(); - } - - private void StopWatsonThread() - { - watsonHelper.Stop(); - - if (Thread.CurrentThread != watsonThread) - { - watsonThread?.Join(); + foreach (WERReport report in e.NewItems) + { + var filePath = report.Executable ?? string.Empty; + + // Filter WER events based on the process we're targeting + if (filePath.Contains(targetProcess.ProcessName, StringComparison.OrdinalIgnoreCase)) + { + WinLogsEntry entry = new(report.TimeStamp, WinLogCategory.Error, report.Description, WinLogsHelper.WERName); + output.Add(entry); + } + } } } @@ -209,8 +202,8 @@ public void LogStateChanged(WinLogsTool logType, bool isEnabled) case WinLogsTool.EventViewer: StartEventViewerThread(); break; - case WinLogsTool.Watson: - StartWatsonThread(); + case WinLogsTool.WER: + ((INotifyCollectionChanged)_werHelper.WERReports).CollectionChanged += WEREvents_CollectionChanged; break; } } @@ -227,8 +220,8 @@ public void LogStateChanged(WinLogsTool logType, bool isEnabled) case WinLogsTool.EventViewer: StopEventViewerThread(); break; - case WinLogsTool.Watson: - StopWatsonThread(); + case WinLogsTool.WER: + ((INotifyCollectionChanged)_werHelper.WERReports).CollectionChanged -= WEREvents_CollectionChanged; break; } } diff --git a/tools/PI/DevHome.PI/Models/WERDisplayInfo.cs b/tools/PI/DevHome.PI/Models/WERDisplayInfo.cs new file mode 100644 index 0000000000..22fa8a6bfd --- /dev/null +++ b/tools/PI/DevHome.PI/Models/WERDisplayInfo.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.IO; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace DevHome.PI.Models; + +public partial class WERDisplayInfo : ObservableObject +{ + public WERReport Report { get; } + + [ObservableProperty] + private string _failureBucket; + + public WERDisplayInfo(WERReport report) + { + Report = report; + FailureBucket = UpdateFailureBucket(); + Report.PropertyChanged += Report_PropertyChanged; + } + + private void Report_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(WERReport.CrashDumpPath)) + { + FailureBucket = UpdateFailureBucket(); + } + else if (e.PropertyName == nameof(WERReport.FailureBucket)) + { + FailureBucket = UpdateFailureBucket(); + } + } + + private string UpdateFailureBucket() + { + // When we provide support for pluggable analysis of cabs, we should call the appropriate analysis tool here to create better failure buckets + return Report.FailureBucket; + } +} diff --git a/tools/PI/DevHome.PI/Models/WERHelper.cs b/tools/PI/DevHome.PI/Models/WERHelper.cs new file mode 100644 index 0000000000..a00a6e98d8 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/WERHelper.cs @@ -0,0 +1,536 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.Eventing.Reader; +using System.IO; +using System.Threading; +using DevHome.Common.Helpers; +using Microsoft.Win32; +using Serilog; + +namespace DevHome.PI.Models; + +/* + * This class is responsible for monitoring the system for WER reports. These reports are generated when an application crashes. + * Additionally it can be used to enable/disable local WER collection for a specific app. To learn more about local WER collection, + * check out https://learn.microsoft.com/windows/win32/wer/collecting-user-mode-dumps + * + * We learn about WER events from either events in the Application event log or from crash dump files on disk. + */ + +internal sealed class WERHelper : IDisposable +{ + private const string WERSubmissionQuery = "(*[System[Provider[@Name=\"Application Error\"]]] and *[System[EventID=1000]])"; + private const string WERReceiveQuery = "(*[System[Provider[@Name=\"Application Error\"]]] and *[System[EventID=1001]])"; + private const string DefaultDumpPath = "%LOCALAPPDATA%\\CrashDumps"; + private const string LocalWERRegistryKey = "SOFTWARE\\Microsoft\\Windows\\Windows Error Reporting\\LocalDumps"; + + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(WERHelper)); + + private readonly Microsoft.UI.Dispatching.DispatcherQueue _dispatcher; + private readonly EventLogWatcher _eventLogWatcher; + private readonly List _filesystemWatchers = []; + private readonly ObservableCollection _werReports = []; + + private List _werLocations = []; + private bool _isRunning; + + public ReadOnlyObservableCollection WERReports { get; private set; } + + public WERHelper() + { + _dispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); + + WERReports = new(_werReports); + + // Subscribe for Application events matching the processName. + EventLogQuery subscriptionQuery = new("Application", PathType.LogName, WERSubmissionQuery); + _eventLogWatcher = new EventLogWatcher(subscriptionQuery); + _eventLogWatcher.EventRecordWritten += new EventHandler(EventLogEventRead); + } + + public void Start() + { + if (!_isRunning) + { + // We get WER events from both the EventLog and from crash dump files on disk. Spin off threads + // to look for existing crash dump files and existing event log events. + ThreadPool.QueueUserWorkItem((o) => + { + _werLocations = GetWERLocations(); + ReadLocalWERReports(); + EnableFileSystemWatchers(); + }); + + ThreadPool.QueueUserWorkItem((o) => + { + ReadWERReportsFromEventLog(); + }); + + _eventLogWatcher.Enabled = true; + + _isRunning = true; + } + } + + public void Stop() + { + if (_isRunning) + { + _eventLogWatcher.Enabled = false; + _isRunning = false; + DisableFileSystemWatchers(); + } + } + + public void Dispose() + { + _eventLogWatcher.Dispose(); + GC.SuppressFinalize(this); + } + + // Check to see if global local WER collection is enabled + // See https://learn.microsoft.com/windows/win32/wer/collecting-user-mode-dumps + // for more details + public bool IsGlobalCollectionEnabled() + { + var key = Registry.LocalMachine.OpenSubKey(LocalWERRegistryKey, false); + + return IsCollectionEnabledForKey(key); + } + + // See if local WER collection is enabled for a specific app + public bool IsCollectionEnabledForApp(string appName) + { + var key = Registry.LocalMachine.OpenSubKey(LocalWERRegistryKey, false); + + // If the local dump key doesn't exist, then app collection is disabled + if (key is null) + { + return false; + } + + var appKey = key.OpenSubKey(appName, false); + + // If the app key doesn't exist, per-app collection isn't enabled. Check the global setting + if (appKey is null) + { + return IsGlobalCollectionEnabled(); + } + + return IsCollectionEnabledForKey(appKey); + } + + private bool IsCollectionEnabledForKey(RegistryKey? key) + { + // If the key doesn't exist, then collection is disabled + if (key is null) + { + return false; + } + + // If the key exists, but dumpcount is set to 0, it's also disabled + if (key.GetValue("DumpCount") is int dumpCount && dumpCount == 0) + { + return false; + } + + // Collection is enabled enabled, but if we're not getting full memory dumps, so cabs may not be + // useful. In this case, report that collection is disabled. + var dumpType = key.GetValue("DumpType") as int?; + if (dumpType is null || dumpType != 2) + { + return false; + } + + // Otherwise it's enabled + return true; + } + + // This changes the registry keys necessary to allow local WER collection for a specific app + public void EnableCollectionForApp(string appname) + { + RuntimeHelper.VerifyCurrentProcessRunningAsAdmin(); + + var globalKey = Registry.LocalMachine.OpenSubKey(LocalWERRegistryKey, true); + + if (globalKey is null) + { + // Need to create the key, and set the global dump collection count to 0 to prevent all apps from generating local dumps + globalKey = Registry.LocalMachine.CreateSubKey(LocalWERRegistryKey); + globalKey.SetValue("DumpCount", 0); + } + + Debug.Assert(globalKey is not null, "Global key is null"); + + var appKey = globalKey.CreateSubKey(appname); + Debug.Assert(appKey is not null, "App key is null"); + + // If dumpcount is set to 0, delete it to enable collection + if (appKey.GetValue("DumpCount") is int dumpCount && dumpCount == 0) + { + appKey.DeleteValue("DumpCount"); + } + + // Make sure the cabs being collected are useful. Go for the full dumps instead of the mini dumps + appKey.SetValue("DumpType", 2); + + return; + } + + // This changes the registry keys necessary to disable local WER collection for a specific app + public void DisableCollectionForApp(string appname) + { + RuntimeHelper.VerifyCurrentProcessRunningAsAdmin(); + + var globalKey = Registry.LocalMachine.OpenSubKey(LocalWERRegistryKey, true); + + if (globalKey is null) + { + // Local collection isn't enabled + return; + } + + var appKey = globalKey.CreateSubKey(appname); + Debug.Assert(appKey is not null, "App key is null"); + + // Set the DumpCount value to 0 to disable collection + appKey.SetValue("DumpCount", 0); + + return; + } + + // Callback that fires when we have a new EventLog message + public void EventLogEventRead(object? obj, EventRecordWrittenEventArgs eventArg) + { + var eventRecord = eventArg.EventRecord; + if (eventRecord != null) + { + if (eventRecord.Id == 1000 && eventRecord.ProviderName.Equals("Application Error", StringComparison.OrdinalIgnoreCase)) + { + var filePath = eventRecord.Properties[10].Value.ToString() ?? string.Empty; + var timeGenerated = eventRecord.TimeCreated ?? DateTime.Now; + var moduleName = eventRecord.Properties[3].Value.ToString() ?? string.Empty; + var executable = eventRecord.Properties[0].Value.ToString() ?? string.Empty; + var eventGuid = eventRecord.Properties[12].Value.ToString() ?? string.Empty; + var description = eventRecord.FormatDescription(); + var pid = eventRecord.Properties[8].Value.ToString() ?? string.Empty; + + FindOrCreateWEREntryFromEventLog(filePath, timeGenerated, moduleName, executable, eventGuid, description, pid); + } + } + } + + private void ReadWERReportsFromEventLog() + { + EventLog eventLog = new("Application"); + + foreach (EventLogEntry entry in eventLog.Entries) + { + if (entry.InstanceId == 1000 + && entry.Source.Equals("Application Error", StringComparison.OrdinalIgnoreCase)) + { + var filePath = entry.ReplacementStrings[10]; + var timeGenerated = entry.TimeGenerated; + var moduleName = entry.ReplacementStrings[3]; + var executable = entry.ReplacementStrings[0]; + var eventGuid = entry.ReplacementStrings[12]; + var description = entry.Message; + var pid = entry.ReplacementStrings[8]; + + FindOrCreateWEREntryFromEventLog(filePath, timeGenerated, moduleName, executable, eventGuid, description, pid); + } + } + } + + private void FindOrCreateWEREntryFromEventLog(string filepath, DateTime timeGenerated, string moduleName, string executable, string eventGuid, string description, string processId) + { + var converter = new Int32Converter(); + var pid = (int?)converter.ConvertFromString(processId); + + // When adding/updating a report, we need to do it on the dispatcher thread + _dispatcher.TryEnqueue(() => + { + // Do we have an entry for this item already (created from the WER files on disk) + var werReport = FindMatchingReport(timeGenerated, executable, pid); + + var createdReport = false; + + if (werReport is null) + { + werReport = new WERReport(); + createdReport = true; + werReport.TimeStamp = timeGenerated; + werReport.Executable = executable; + werReport.Pid = pid ?? 0; + } + + // Populate the report + werReport.FilePath = filepath; + werReport.Module = moduleName; + werReport.EventGuid = eventGuid; + werReport.Description = description; + werReport.FailureBucket = GenerateFailureBucketFromEventLogDescription(description); + + // Don't add the report until it's fully populated so we have as much information as possible for our listeners + if (createdReport) + { + _werReports.Add(werReport); + } + }); + } + + private void FindOrCreateWEREntryFromLocalDumpFile(string crashDumpFile) + { + var timeGenerated = File.GetCreationTime(crashDumpFile); + + // The crashdumpFilename has a format of + // executable.pid.dmp + // so it could be + // a.exe.40912.dmp + // but also + // a.b.exe.40912.dmp + // Parse the filename starting from the back + + // Find the last dot index + var dmpExtensionIndex = crashDumpFile.LastIndexOf('.'); + if (dmpExtensionIndex == -1) + { + _log.Information("Unexpected crash dump filename: " + crashDumpFile); + return; + } + + // Remove the .dmp. This should give us a string like a.b.exe.40912 + var filenameWithNoDmp = crashDumpFile.Substring(0, dmpExtensionIndex); + + // Find the PID + var pidIndex = filenameWithNoDmp.LastIndexOf('.'); + if (pidIndex == -1) + { + _log.Information("Unexpected crash dump filename: " + crashDumpFile); + return; + } + + var processID = filenameWithNoDmp.Substring(pidIndex + 1); + + // Now peel off the PID. This should give us a.b.exe + var executableFullPath = filenameWithNoDmp.Substring(0, pidIndex); + + FileInfo fileInfo = new(executableFullPath); + + var converter = new Int32Converter(); + var pid = (int?)converter.ConvertFromString(processID); + + _dispatcher.TryEnqueue(() => + { + // Do we have an entry for this item already (created from the event log entry) + var werReport = FindMatchingReport(timeGenerated, fileInfo.Name, pid); + + var createdReport = false; + + if (werReport is null) + { + werReport = new WERReport(); + createdReport = true; + werReport.TimeStamp = timeGenerated; + werReport.Executable = fileInfo.Name; + werReport.Pid = pid ?? 0; + } + + // Populate the report + werReport.CrashDumpPath = crashDumpFile; + + // Don't add the report until it's fully populated so we have as much information as possible for our listeners + if (createdReport) + { + _werReports.Add(werReport); + } + }); + + return; + } + + private WERReport? FindMatchingReport(DateTime timestamp, string executable, int? pid) + { + Debug.Assert(_dispatcher.HasThreadAccess, "This method should only be called on the dispatcher thread"); + Debug.Assert(timestamp.Kind == DateTimeKind.Local, "TimeGenerated should be in local time"); + var timestampIndex = timestamp.Ticks; + + // It's a match if the timestamp is within 2 minute of the event log entry + var ticksWindow = new TimeSpan(0, 2, 0).Ticks; + + WERReport? werReport = null; + + // See if we can find a matching entry in the list + foreach (var report in _werReports) + { + if (report.Executable.Equals(executable, StringComparison.OrdinalIgnoreCase) && report.Pid == pid) + { + // See if the timestamps are "close enough" + Debug.Assert(report.TimeStamp.Kind == DateTimeKind.Local, "TimeGenerated should be in local time"); + var ticksDiff = Math.Abs(report.TimeStamp.Ticks - timestampIndex); + + if (ticksDiff < ticksWindow) + { + werReport = report; + break; + } + } + } + + return werReport; + } + + private string GenerateFailureBucketFromEventLogDescription(string description) + { + /* The description can look like this + + Faulting application name: DevHome.PI.exe, version: 1.0.0.0, time stamp: 0x66470000 + Faulting module name: KERNELBASE.dll, version: 10.0.22621.3810, time stamp: 0x10210ca8 + Exception code: 0xe0434352 + Fault offset: 0x000000000005f20c + Faulting process id: 0x0xa078 + Faulting application start time: 0x0x1dad175bd05dea9 + Faulting application path: E:\devhome\src\bin\x64\Debug\net8.0-windows10.0.22621.0\AppX\DevHome.PI.exe + Faulting module path: C:\WINDOWS\System32\KERNELBASE.dll + Report Id: 7a4cd0a8-f65b-4f27-b250-cc5bd57e39d6 + Faulting package full name: Microsoft.Windows.DevHome.Dev_0.0.0.0_x64__8wekyb3d8bbwe + Faulting package-relative application ID: Devhome.PI + + Let's create a placeholder failure bucket based on the module name, offset, and exception code. In the above example, + we'll generate a bucket "KERNELBASE.dll+0x000000000005f20c 0xe0434352" + */ + + var lines = description.Split('\n', StringSplitOptions.RemoveEmptyEntries); + string? moduleName = null; + string? exceptionCode = null; + string? faultOffset = null; + + foreach (var line in lines) + { + if (line.Contains("Fault offset:")) + { + faultOffset = line.Substring(line.IndexOf(':') + 1).Trim(); + } + else if (line.Contains("Exception code:")) + { + exceptionCode = line.Substring(line.IndexOf(':') + 1).Trim(); + } + else if (line.Contains("Faulting module name:")) + { + var startIndex = line.IndexOf(':') + 1; + var endIndex = line.IndexOf(',') - 1; + + moduleName = line.Substring(startIndex, endIndex - startIndex + 1).Trim(); + } + } + + if (moduleName is not null && exceptionCode is not null && faultOffset is not null) + { + return $"{moduleName}+{faultOffset} {exceptionCode}"; + } + + return string.Empty; + } + + private void ReadLocalWERReports() + { + foreach (var dumpLocation in _werLocations) + { + try + { + // Enumerate all of the existing dump files in this location + foreach (var dumpFile in Directory.EnumerateFiles(dumpLocation, "*.dmp")) + { + FindOrCreateWEREntryFromLocalDumpFile(dumpFile); + } + } + catch + { + _log.Error("Error enumerating directory " + dumpLocation); + } + } + } + + // Generate a list of all of the locations on disk where local WER dumps are stored + private List GetWERLocations() + { + var list = new List(); + + var key = Registry.LocalMachine.OpenSubKey(LocalWERRegistryKey, false); + + if (key is not null) + { + var globaldumppath = GetDumpPath(key); + + Debug.Assert(globaldumppath is not null, "Global dump path is not set"); + list.Add(globaldumppath); + + var subKeys = key.GetSubKeyNames(); + foreach (var subkey in subKeys) + { + var dumpPath = GetDumpPath(key.OpenSubKey(subkey)); + + if (dumpPath is not null) + { + // If this item isn't in the list, add it. + if (!list.Contains(dumpPath)) + { + list.Add(dumpPath); + } + } + } + } + + return list; + } + + private string? GetDumpPath(RegistryKey? key) + { + if (key is not null) + { + if (key.GetValue("DumpFolder") is not string dumpFolder) + { + // If a dumppath isn't explicitly set, then use the system's default dump path + dumpFolder = DefaultDumpPath; + } + + return Environment.ExpandEnvironmentVariables(dumpFolder); + } + + return null; + } + + // Enable watchers to catch new WER dumps as they are generated + private void EnableFileSystemWatchers() + { + _filesystemWatchers.Clear(); + + foreach (var path in _werLocations) + { + // If this directory exists, monitor it for new files + if (Directory.Exists(path)) + { + var watcher = new FileSystemWatcher(path); + watcher.Created += (sender, e) => + { + _log.Information($"New dump file: {e.FullPath}"); + FindOrCreateWEREntryFromLocalDumpFile(e.FullPath); + }; + + watcher.EnableRaisingEvents = true; + _filesystemWatchers.Add(watcher); + } + } + } + + private void DisableFileSystemWatchers() + { + _filesystemWatchers.Clear(); + } +} diff --git a/tools/PI/DevHome.PI/Models/WERReport.cs b/tools/PI/DevHome.PI/Models/WERReport.cs new file mode 100644 index 0000000000..3463217c10 --- /dev/null +++ b/tools/PI/DevHome.PI/Models/WERReport.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Globalization; +using System.Runtime.CompilerServices; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace DevHome.PI.Models; + +public partial class WERReport : ObservableObject +{ + [ObservableProperty] + private DateTime _timeStamp; + + public string TimeGenerated => TimeStamp.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.CurrentCulture); + + [ObservableProperty] + private string _filePath = string.Empty; + + [ObservableProperty] + private string _module = string.Empty; + + [ObservableProperty] + private string _executable = string.Empty; + + [ObservableProperty] + private string _eventGuid = string.Empty; + + [ObservableProperty] + private string _description = string.Empty; + + [ObservableProperty] + private int _pid = 0; + + [ObservableProperty] + private string _crashDumpPath = string.Empty; + + [ObservableProperty] + private string _failureBucket = string.Empty; + + public WERReport() + { + } +} diff --git a/tools/PI/DevHome.PI/Models/WatsonReport.cs b/tools/PI/DevHome.PI/Models/WatsonReport.cs deleted file mode 100644 index bee4d89c42..0000000000 --- a/tools/PI/DevHome.PI/Models/WatsonReport.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Globalization; - -namespace DevHome.PI.Models; - -public class WatsonReport -{ - private readonly DateTime timeGenerated; - - public string TimeGenerated => timeGenerated.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.CurrentCulture); - - public string Module { get; } - - public string Executable { get; } - - public string EventGuid { get; } - - public string? WatsonLog { get; set; } - - public string? WatsonReportFile { get; set; } - - public WatsonReport(DateTime timeGenerated, string moduleName, string executable, string eventGuid) - { - this.timeGenerated = timeGenerated; - Module = moduleName; - Executable = executable; - EventGuid = eventGuid; - } -} diff --git a/tools/PI/DevHome.PI/Models/WinLogsEntry.cs b/tools/PI/DevHome.PI/Models/WinLogsEntry.cs index 4ead017e24..aea90eeb36 100644 --- a/tools/PI/DevHome.PI/Models/WinLogsEntry.cs +++ b/tools/PI/DevHome.PI/Models/WinLogsEntry.cs @@ -75,5 +75,5 @@ public enum WinLogsTool ETWLogs, DebugOutput, EventViewer, - Watson, + WER, } diff --git a/tools/PI/DevHome.PI/PIApp.xaml.cs b/tools/PI/DevHome.PI/PIApp.xaml.cs index 16c6802da3..2ee3dad5e0 100644 --- a/tools/PI/DevHome.PI/PIApp.xaml.cs +++ b/tools/PI/DevHome.PI/PIApp.xaml.cs @@ -4,6 +4,7 @@ using System; using DevHome.Common.Extensions; using DevHome.Common.Services; +using DevHome.PI.Models; using DevHome.PI.Pages; using DevHome.PI.Services; using DevHome.PI.Telemetry; @@ -45,11 +46,10 @@ public App() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Window - // Provide an explicit implementationInstance otherwise AddSingleton does not create a new instance immediately. - // It will lazily init when the first component requires it but the hotkey helper needs to be registered immediately. - services.AddSingleton(new PrimaryWindow()); + services.AddSingleton(); // Views and ViewModels services.AddSingleton(); @@ -62,8 +62,8 @@ public App() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -79,6 +79,10 @@ public App() services.AddTransient(); services.AddTransient(); }).Build(); + + // Provide an explicit implementationInstance otherwise AddSingleton does not create a new instance immediately. + // It will lazily init when the first component requires it but the hotkey helper needs to be registered immediately. + Application.Current.GetService(); } internal static bool IsFeatureEnabled() diff --git a/tools/PI/DevHome.PI/Pages/WERPage.xaml b/tools/PI/DevHome.PI/Pages/WERPage.xaml new file mode 100644 index 0000000000..22dd36bdf1 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/WERPage.xaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/Pages/WERPage.xaml.cs b/tools/PI/DevHome.PI/Pages/WERPage.xaml.cs new file mode 100644 index 0000000000..6bf3f74224 --- /dev/null +++ b/tools/PI/DevHome.PI/Pages/WERPage.xaml.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using CommunityToolkit.WinUI.UI.Controls; +using DevHome.Common.Extensions; +using DevHome.PI.Models; +using DevHome.PI.Telemetry; +using DevHome.PI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace DevHome.PI.Pages; + +public sealed partial class WERPage : Page +{ + private WERPageViewModel ViewModel { get; } + + public WERPage() + { + ViewModel = Application.Current.GetService(); + InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + Application.Current.GetService().SwitchTo(Feature.WERReports); + } + + private void SelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args) + { + UpdateInfoBox(); + } + + private void WERDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + UpdateInfoBox(); + } + + private void UpdateInfoBox() + { + if (WERDataGrid.SelectedItem is null) + { + WERInfo.Text = string.Empty; + return; + } + + SelectorBarItem selectedItem = InfoSelector.SelectedItem; + int currentSelectedIndex = InfoSelector.Items.IndexOf(selectedItem); + WERDisplayInfo info = (WERDisplayInfo)WERDataGrid.SelectedItem; + + switch (currentSelectedIndex) + { + case 0: // WER info + WERInfo.Text = info.Report.Description; + break; + } + } + + private void WERDataGrid_Sorting(object sender, DataGridColumnEventArgs e) + { + if (e.Column.Tag is not null) + { + bool sortAscending = e.Column.SortDirection == DataGridSortDirection.Ascending; + + // Flip the sort direction + sortAscending = !sortAscending; + + string? tag = e.Column.Tag.ToString(); + Debug.Assert(tag is not null, "Why is the tag null?"); + + if (tag == "DateTime") + { + ViewModel.SortByDateTime(sortAscending); + } + else if (tag == "FaultingExecutable") + { + ViewModel.SortByFaultingExecutable(sortAscending); + } + else if (tag == "WERBucket") + { + ViewModel.SortByWERBucket(sortAscending); + } + else if (tag == "CrashDumpPath") + { + ViewModel.SortByCrashDumpPath(sortAscending); + } + + e.Column.SortDirection = sortAscending ? DataGridSortDirection.Ascending : DataGridSortDirection.Descending; + + // Clear the sort direction for the other columns + foreach (DataGridColumn column in WERDataGrid.Columns) + { + if (column != e.Column) + { + column.SortDirection = null; + } + } + } + } + + private void LocalDumpCollection_Toggled(object sender, RoutedEventArgs e) + { + ViewModel.ChangeLocalCollectionForApp(LocalDumpCollectionToggle.IsOn); + } + + private void HyperlinkButton_Click(object sender, RoutedEventArgs e) + { + HyperlinkButton? hyperlinkButton = sender as HyperlinkButton; + Debug.Assert(hyperlinkButton is not null, "Who called HyperlinkButton_Click that wasn't a hyperlink button?"); + + WERDisplayInfo? info = hyperlinkButton.Tag as WERDisplayInfo; + Debug.Assert(info is not null, "This object should have a Tag with a WERDisplayInfo"); + + ViewModel.OpenCab(info.Report.CrashDumpPath); + } +} diff --git a/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml b/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml deleted file mode 100644 index ba09c74989..0000000000 --- a/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml.cs b/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml.cs deleted file mode 100644 index c3e8345a66..0000000000 --- a/tools/PI/DevHome.PI/Pages/WatsonsPage.xaml.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using DevHome.Common.Extensions; -using DevHome.PI.Telemetry; -using DevHome.PI.ViewModels; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Navigation; - -namespace DevHome.PI.Pages; - -public sealed partial class WatsonsPage : Page, IDisposable -{ - private WatsonPageViewModel ViewModel { get; } - - public WatsonsPage() - { - ViewModel = Application.Current.GetService(); - InitializeComponent(); - } - - protected override void OnNavigatedTo(NavigationEventArgs e) - { - base.OnNavigatedTo(e); - Application.Current.GetService().SwitchTo(Feature.WERReports); - } - - public void Dispose() - { - ViewModel.Dispose(); - GC.SuppressFinalize(this); - } -} diff --git a/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml b/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml index 27a5cdd84d..c7384f9e3b 100644 --- a/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml +++ b/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml @@ -36,9 +36,9 @@ Checked="{x:Bind ViewModel.LogStateChanged}" Unchecked="{x:Bind ViewModel.LogStateChanged}" Margin="0,0,8,0" Content="{x:Bind helpers:WinLogsHelper.EventViewerName}"/> + Content="{x:Bind helpers:WinLogsHelper.WERName}"/> public sealed partial class ContentDialogWithNonInteractiveContent : ContentDialog { + private readonly IThemeSelectorService _themeSelector; + + private readonly DevHomeContentDialogContent _content; + public ContentDialogWithNonInteractiveContent(DevHomeContentDialogContent content) { this.InitializeComponent(); + _themeSelector = Application.Current.GetService(); + // Since we use the renderer service to allow the card to receive theming updates, we need to ensure the UI thread is used. var dispatcherQueue = Application.Current.GetService(); dispatcherQueue.TryEnqueue(async () => { Title = content.Title; PrimaryButtonText = content.PrimaryButtonText; - var rendererService = Application.Current.GetService(); - var renderer = await rendererService.GetRendererAsync(); - renderer.HostConfig.ContainerStyles.Default.BackgroundColor = Microsoft.UI.Colors.Transparent; - var card = renderer.RenderAdaptiveCardFromJsonString(content.ContentDialogInternalAdaptiveCardJson?.Stringify() ?? string.Empty); - Content = new ScrollViewer - { - VerticalScrollBarVisibility = ScrollBarVisibility.Auto, - Content = card.FrameworkElement, - }; + Content = await MakeCardContentAsync(); + + // Set the theme of the content dialog box + RequestedTheme = _themeSelector.IsDarkTheme() ? ElementTheme.Dark : ElementTheme.Light; SecondaryButtonText = content.SecondaryButtonText; this.Focus(FocusState.Programmatic); }); + + _themeSelector.ThemeChanged += OnThemeChanged; + _content = content; + } + + private async void OnThemeChanged(object? sender, ElementTheme newRequestedTheme) + { + // set the theme of the content dialog box. + RequestedTheme = newRequestedTheme; + Content = await MakeCardContentAsync(); + } + + private async Task MakeCardContentAsync() + { + var rendererService = Application.Current.GetService(); + var renderer = await rendererService.GetRendererAsync(); + renderer.HostConfig.ContainerStyles.Default.BackgroundColor = Microsoft.UI.Colors.Transparent; + var card = renderer.RenderAdaptiveCardFromJsonString(_content.ContentDialogInternalAdaptiveCardJson?.Stringify() ?? string.Empty); + + return new ScrollViewer + { + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + Content = card.FrameworkElement, + }; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/QuickstartPlaygroundView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/QuickstartPlaygroundView.xaml.cs index fce41c3142..0eb87deca8 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/QuickstartPlaygroundView.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/QuickstartPlaygroundView.xaml.cs @@ -8,6 +8,7 @@ using DevHome.Common.Extensions; using DevHome.Common.Services; using DevHome.Common.Views; +using DevHome.Contracts.Services; using DevHome.SetupFlow.ViewModels; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -21,8 +22,24 @@ public sealed partial class QuickstartPlaygroundView : UserControl { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(QuickstartPlaygroundView)); + private readonly IThemeSelectorService _themeSelector; + private ContentDialog? _adaptiveCardContentDialog; + /// + /// Used to keep track of the current adaptive card session. + /// The session should not be reloaded in the same QSP instance with the same provider. + /// If it is, Content of the content dialog might change depending on + /// user actions between reloads of _adaptiveCardSession2. + /// + private IExtensionAdaptiveCardSession2? _adaptiveCardSession2; + + /// + /// Because QSP can use different providers, the session *should* be reloaded + /// if the provider changes. + /// + private bool _shouldReloadAdaptiveCardSession; + public QuickstartPlaygroundViewModel ViewModel { get; set; @@ -30,6 +47,8 @@ public QuickstartPlaygroundViewModel ViewModel public QuickstartPlaygroundView() { + _themeSelector = Application.Current.GetService(); + _themeSelector.ThemeChanged += OnThemeChanged; ViewModel = Application.Current.GetService(); ViewModel.PropertyChanged += ViewModel_PropertyChanged; this.InitializeComponent(); @@ -88,8 +107,10 @@ private async void ExtensionProviderComboBox_Loading(FrameworkElement sender, ob private async void ExtensionProviderComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { + _shouldReloadAdaptiveCardSession = true; ViewModel.OnQuickstartSelectionChanged(); await ShowExtensionInitializationUI(); + _shouldReloadAdaptiveCardSession = false; } private void NegativeFeedbackConfirmation_Click(object sender, RoutedEventArgs e) @@ -148,49 +169,128 @@ public void OnAdaptiveCardSessionStopped(IExtensionAdaptiveCardSession2 cardSess } } - private async Task ShowAdaptiveCardOnContentDialog(QuickStartProjectAdaptiveCardResult adaptiveCardSessionResult) + private async Task ShowAdaptiveCardOnContentDialog() { - if (adaptiveCardSessionResult == null) + var extensionAdaptiveCardPanel = await SetUpAdaptiveCardAsync(); + if (extensionAdaptiveCardPanel == null || _adaptiveCardSession2 == null) { // No adaptive card to show (i.e. no dependencies or AI initialization). return; } - if (adaptiveCardSessionResult.Result.Status == ProviderOperationStatus.Failure) + _adaptiveCardSession2.Stopped += OnAdaptiveCardSessionStopped; + + _adaptiveCardContentDialog = new ContentDialog { - _log.Error($"Failed to show adaptive card. {adaptiveCardSessionResult.Result.DisplayMessage} - {adaptiveCardSessionResult.Result.DiagnosticText}"); - return; - } + XamlRoot = this.XamlRoot, + Content = extensionAdaptiveCardPanel, + + // Set the theme of the content dialog box + RequestedTheme = _themeSelector.IsDarkTheme() ? ElementTheme.Dark : ElementTheme.Light, + }; + + await _adaptiveCardContentDialog.ShowAsync(); + + _adaptiveCardSession2.Dispose(); + _adaptiveCardContentDialog = null; + } - var adapativeCardController = adaptiveCardSessionResult.AdaptiveCardSession; + /// + /// Makes the adaptive card panel to use in the content dialog. Will check if + /// an adaptive card session is made and if not, make one. + /// + /// An awatible task that has an ExtensionAdaptiveCardPanel. Returns null if + /// the session can not be made. + private async Task SetUpAdaptiveCardAsync() + { var extensionAdaptiveCardPanel = new ExtensionAdaptiveCardPanel(); var renderingService = Application.Current.GetService(); var renderer = await renderingService.GetRendererAsync(); - extensionAdaptiveCardPanel.Bind(adapativeCardController, renderer); - extensionAdaptiveCardPanel.RequestedTheme = ActualTheme; + // Make the session if not already. + SaveAdaptiveCardSession(); + if (_adaptiveCardSession2 == null) + { + return null; + } - adapativeCardController.Stopped += OnAdaptiveCardSessionStopped; + extensionAdaptiveCardPanel.Bind(_adaptiveCardSession2, renderer); + extensionAdaptiveCardPanel.RequestedTheme = _themeSelector.IsDarkTheme() ? ElementTheme.Dark : ElementTheme.Light; - _adaptiveCardContentDialog = new ContentDialog + return extensionAdaptiveCardPanel; + } + + /// + /// Changed the dialog theme if the windows theme changes. This is done in 3 steps. + /// 1. Reload the renderer. This forces the renderer to use the correct host config file. + /// 2. Reload the adaptive card (Adaptive card content will not change themes unless re-loaded) + /// 3. Replace the Content of the content dialog. + /// + /// Unused + /// The new theme + private async void OnThemeChanged(object? sender, ElementTheme newRequestedTheme) + { + RequestedTheme = newRequestedTheme; + + if (_adaptiveCardContentDialog == null) { - XamlRoot = this.XamlRoot, - Content = extensionAdaptiveCardPanel, - }; + return; + } - await _adaptiveCardContentDialog.ShowAsync(); + if (_adaptiveCardSession2 == null) + { + return; + } - adapativeCardController.Dispose(); - _adaptiveCardContentDialog = null; + var extensionPanel = await SetUpAdaptiveCardAsync(); + if (extensionPanel == null) + { + return; + } + + _adaptiveCardContentDialog.Content = extensionPanel; + _adaptiveCardContentDialog.RequestedTheme = _themeSelector.IsDarkTheme() ? ElementTheme.Dark : ElementTheme.Light; + + // Resetting Content breaks all events. Re-hook the close button event. + _adaptiveCardSession2.Stopped += OnAdaptiveCardSessionStopped; } public async Task ShowExtensionInitializationUI() { if (ViewModel.ActiveQuickstartSelection is not null) { - var adaptiveCardSessionResult = ViewModel.ActiveQuickstartSelection.CreateAdaptiveCardSessionForExtensionInitialization(ViewModel.ActivityId); - await ShowAdaptiveCardOnContentDialog(adaptiveCardSessionResult); + await ShowAdaptiveCardOnContentDialog(); + } + } + + /// + /// Gets the adaptive card session from the provider and saves it. + /// Session will not reload if + /// 1. The provider did not change. + /// 2. The session is not null. + /// + private void SaveAdaptiveCardSession() + { + if (!_shouldReloadAdaptiveCardSession || _adaptiveCardSession2 is not null) + { + return; + } + + var adaptiveCardSessionResult = ViewModel.ActiveQuickstartSelection?.CreateAdaptiveCardSessionForExtensionInitialization(ViewModel.ActivityId); + if (adaptiveCardSessionResult == null) + { + _adaptiveCardSession2 = null; + return; } + + if (adaptiveCardSessionResult.Result.Status == ProviderOperationStatus.Failure) + { + _log.Error($"Failed to show adaptive card. {adaptiveCardSessionResult.Result.DisplayMessage} - {adaptiveCardSessionResult.Result.DiagnosticText}"); + _adaptiveCardSession2 = null; + return; + } + + _adaptiveCardSession2 = adaptiveCardSessionResult.AdaptiveCardSession; } public async Task ShowProgressAdaptiveCard() @@ -206,7 +306,7 @@ public async Task ShowProgressAdaptiveCard() var renderer = await renderingService.GetRendererAsync(); extensionAdaptiveCardPanel.Bind(progressAdaptiveCardSession, renderer); - extensionAdaptiveCardPanel.RequestedTheme = ActualTheme; + extensionAdaptiveCardPanel.RequestedTheme = _themeSelector.IsDarkTheme() ? ElementTheme.Dark : ElementTheme.Light; extensionAdaptiveCardPanel.UiUpdate += (object? sender, FrameworkElement e) => { From c628a7df4f160f353a36c5a1a2ec3761286de58c Mon Sep 17 00:00:00 2001 From: Lauren Ciha <64796985+lauren-ciha@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:48:07 -0700 Subject: [PATCH 24/82] Added tooltip for the "More button (. . .)" in the added environments list item (#3442) * Added more tooltip * Changed button attributes to x:Uid --- .../Strings/en-us/Resources.resw | 10 +++- .../Views/LandingPage.xaml | 55 ++++++++++--------- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw index 01972f5c96..d820b1ecb3 100644 --- a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw +++ b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw @@ -344,11 +344,19 @@ Text for when the environment page in Dev Home is empty and the user has no extensions installed that support environments - More Options + More options The text narrator should say for the ... button Sync Accessible name for the button to sync environments. + + More options + ToolTip for the button that displays "More Options" menu + + + More options + Name of the button that brings up the "More options" menu + \ No newline at end of file diff --git a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml index 77fdad883f..3e5ed059fd 100644 --- a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml +++ b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml @@ -35,38 +35,38 @@ - - - - - - - - - - - - + + + + + + + + + + + + public string Intent { get; } + /// + /// Gets the name of the module that this configuration unit belongs to. + /// + public string ModuleName { get; } + /// /// Gets the values of the configuration units that this unit depends on. /// @@ -43,7 +55,8 @@ public interface IDSCUnit public IList> Metadata { get; } /// - /// Gets the information on the origin of the configuration unit if available. + /// Gets the details of the configuration unit. /// - public IDSCUnitDetails Details { get; } + /// The details of the configuration unit. + public Task GetDetailsAsync(); } diff --git a/services/DevHome.Services.DesiredStateConfiguration/Models/DSCSet.cs b/services/DevHome.Services.DesiredStateConfiguration/Models/DSCSet.cs index 39580427cd..802b7ac356 100644 --- a/services/DevHome.Services.DesiredStateConfiguration/Models/DSCSet.cs +++ b/services/DevHome.Services.DesiredStateConfiguration/Models/DSCSet.cs @@ -11,6 +11,24 @@ namespace DevHome.Services.DesiredStateConfiguration.Models; internal sealed class DSCSet : IDSCSet { + /// + public Guid InstanceIdentifier { get; } + + /// + public string Name { get; } + + /// + public IReadOnlyList Units => UnitsInternal.AsReadOnly(); + + /// + /// Gets the list of units in this set. + /// + /// + /// This list maintains the concrete type of the objects which is internal + /// to this service project. + /// + internal IList UnitsInternal { get; } + public DSCSet(ConfigurationSet configSet) { // Constructor copies all the required data from the out-of-proc COM @@ -19,12 +37,6 @@ public DSCSet(ConfigurationSet configSet) // longer available (e.g. AppInstaller service is no longer running). InstanceIdentifier = configSet.InstanceIdentifier; Name = configSet.Name; - Units = configSet.Units.Select(unit => new DSCUnit(unit)).ToList(); + UnitsInternal = configSet.Units.Select(unit => new DSCUnit(unit)).ToList(); } - - public Guid InstanceIdentifier { get; } - - public string Name { get; } - - public IReadOnlyList Units { get; } } diff --git a/services/DevHome.Services.DesiredStateConfiguration/Models/DSCUnit.cs b/services/DevHome.Services.DesiredStateConfiguration/Models/DSCUnit.cs index 3fb608d3bf..f9d3a2b11a 100644 --- a/services/DevHome.Services.DesiredStateConfiguration/Models/DSCUnit.cs +++ b/services/DevHome.Services.DesiredStateConfiguration/Models/DSCUnit.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using DevHome.Services.DesiredStateConfiguration.Contracts; using Microsoft.Management.Configuration; @@ -12,6 +14,35 @@ internal sealed class DSCUnit : IDSCUnit { private const string DescriptionMetadataKey = "description"; private const string ModuleMetadataKey = "module"; + private readonly IDSCUnitDetails _defaultDetails; + private Task _loadDetailsTask; + + /// + public string Type { get; } + + /// + public string Id { get; } + + /// + public Guid InstanceId { get; } + + /// + public string Description { get; } + + /// + public string Intent { get; } + + /// + public string ModuleName { get; } + + /// + public IList Dependencies { get; } + + /// + public IList> Settings { get; } + + /// + public IList> Metadata { get; } public DSCUnit(ConfigurationUnit unit) { @@ -21,6 +52,7 @@ public DSCUnit(ConfigurationUnit unit) // longer available (e.g. AppInstaller service is no longer running). Type = unit.Type; Id = unit.Identifier; + InstanceId = unit.InstanceIdentifier; Intent = unit.Intent.ToString(); Dependencies = [.. unit.Dependencies]; @@ -32,33 +64,27 @@ public DSCUnit(ConfigurationUnit unit) Settings = unit.Settings.Select(s => new KeyValuePair(s.Key, s.Value.ToString())).ToList(); Metadata = unit.Metadata.Select(m => new KeyValuePair(m.Key, m.Value.ToString())).ToList(); - // Load details if available, otherwise create empty details with just - // the module name if available - if (unit.Details == null) - { - // Get module name from metadata - unit.Metadata.TryGetValue(ModuleMetadataKey, out var moduleObj); - Details = new DSCUnitDetails(moduleObj?.ToString() ?? string.Empty); - } - else - { - Details = new DSCUnitDetails(unit.Details); - } - } - - public string Type { get; } + // Get module name from metadata + ModuleName = Metadata.FirstOrDefault(m => m.Key == ModuleMetadataKey).Value?.ToString() ?? string.Empty; - public string Id { get; } - - public string Description { get; } - - public string Intent { get; } - - public IList Dependencies { get; } - - public IList> Settings { get; } + // Build default details + _defaultDetails = unit.Details == null ? new DSCUnitDetails(ModuleName) : new DSCUnitDetails(unit.Details); + _loadDetailsTask = Task.FromResult(_defaultDetails); + } - public IList> Metadata { get; } + /// + public async Task GetDetailsAsync() + { + var loadedDetails = await _loadDetailsTask; + return loadedDetails ?? _defaultDetails; + } - public IDSCUnitDetails Details { get; } + /// + /// Set an asynchronous task to load the details for the unit in the background. + /// + /// Task to load the details + internal void SetLoadDetailsTask(Task loadDetailsTask) + { + _loadDetailsTask = loadDetailsTask; + } } diff --git a/services/DevHome.Services.DesiredStateConfiguration/Services/DSCOperations.cs b/services/DevHome.Services.DesiredStateConfiguration/Services/DSCOperations.cs index 048dc300b3..4318cfb25b 100644 --- a/services/DevHome.Services.DesiredStateConfiguration/Services/DSCOperations.cs +++ b/services/DevHome.Services.DesiredStateConfiguration/Services/DSCOperations.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Linq; using System.Threading.Tasks; using DevHome.Services.DesiredStateConfiguration.Contracts; using DevHome.Services.DesiredStateConfiguration.Exceptions; @@ -44,8 +45,32 @@ public async Task GetConfigurationUnitDetailsAsync(IDSCFile file) var configSet = await OpenConfigurationSetAsync(file, processor); _logger.LogInformation("Getting configuration unit details"); - await processor.GetSetDetailsAsync(configSet, ConfigurationUnitDetailFlags.ReadOnly); - return new DSCSet(configSet); + var detailsOperation = processor.GetSetDetailsAsync(configSet, ConfigurationUnitDetailFlags.ReadOnly); + var detailsOperationTask = detailsOperation.AsTask(); + + var set = new DSCSet(configSet); + + // For each DSC unit, create a task to get the details asynchronously + // in the background + foreach (var unit in set.UnitsInternal) + { + unit.SetLoadDetailsTask(Task.Run(async () => + { + try + { + await detailsOperationTask; + _logger.LogInformation($"Settings details for unit {unit.InstanceId}"); + return GetCompleteUnitDetails(configSet, unit.InstanceId); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to get details for unit {unit.InstanceId}"); + return null; + } + })); + } + + return set; } /// @@ -157,4 +182,30 @@ private static async Task StringToStreamAsync(string result.Seek(0); return result; } + + /// + /// Gets the complete details for a unit if available. + /// + /// Configuration set + /// Unit instance ID + /// Complete unit details if available, otherwise null + private DSCUnitDetails GetCompleteUnitDetails(ConfigurationSet configSet, Guid instanceId) + { + var unitFound = configSet.Units.FirstOrDefault(u => u.InstanceIdentifier == instanceId); + if (unitFound == null) + { + _logger.LogWarning($"Unit {instanceId} not found in the configuration set. No further details will be available to the unit."); + return null; + } + + if (unitFound.Details == null) + { + _logger.LogWarning($"Details for unit {instanceId} not found. No further details will be available to the unit."); + return null; + } + + // After GetSetDetailsAsync completes, the Details property will be + // populated if the details were found. + return new DSCUnitDetails(unitFound.Details); + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DSCConfigurationUnitDetailsViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DSCConfigurationUnitDetailsViewModel.cs new file mode 100644 index 0000000000..9d6f7ba55b --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DSCConfigurationUnitDetailsViewModel.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Services.DesiredStateConfiguration.Contracts; + +namespace DevHome.SetupFlow.ViewModels; + +public class DSCConfigurationUnitDetailsViewModel +{ + private readonly IDSCUnitDetails _unitDetails; + + public DSCConfigurationUnitDetailsViewModel(IDSCUnitDetails unitDetails) + { + _unitDetails = unitDetails; + } + + public string UnitType => _unitDetails.UnitType; + + public string UnitDescription => _unitDetails.UnitDescription; + + public string UnitDocumentationUri => _unitDetails.UnitDocumentationUri; + + public string ModuleName => _unitDetails.ModuleName; + + public string ModuleType => _unitDetails.ModuleType; + + public string ModuleSource => _unitDetails.ModuleSource; + + public string ModuleDescription => _unitDetails.ModuleDescription; + + public string ModuleDocumentationUri => _unitDetails.ModuleDocumentationUri; + + public string PublishedModuleUri => _unitDetails.PublishedModuleUri; + + public string Version => _unitDetails.Version; + + public bool IsLocal => _unitDetails.IsLocal; + + public string Author => _unitDetails.Author; + + public string Publisher => _unitDetails.Publisher; + + public bool IsPublic => _unitDetails.IsPublic; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DSCConfigurationUnitViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DSCConfigurationUnitViewModel.cs index 18ce3fe79b..14fdfdf2bf 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DSCConfigurationUnitViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DSCConfigurationUnitViewModel.cs @@ -2,18 +2,19 @@ // Licensed under the MIT License. using System.Collections.Generic; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using DevHome.Services.DesiredStateConfiguration.Contracts; namespace DevHome.SetupFlow.ViewModels; -public class DSCConfigurationUnitViewModel +public partial class DSCConfigurationUnitViewModel : ObservableObject { private readonly IDSCUnit _configurationUnit; - public DSCConfigurationUnitViewModel(IDSCUnit configurationUnit) - { - _configurationUnit = configurationUnit; - } + [ObservableProperty] + private DSCConfigurationUnitDetailsViewModel _details; public string Id => _configurationUnit.Id; @@ -25,39 +26,18 @@ public DSCConfigurationUnitViewModel(IDSCUnit configurationUnit) public string Intent => _configurationUnit.Intent; + public string ModuleName => _configurationUnit.ModuleName; + public IList Dependencies => _configurationUnit.Dependencies; public IList> Settings => _configurationUnit.Settings; public IList> Metadata => _configurationUnit.Metadata; - public string UnitType => _configurationUnit.Details.UnitType; - - public string UnitDescription => _configurationUnit.Details.UnitDescription; - - public string UnitDocumentationUri => _configurationUnit.Details.UnitDocumentationUri; - - public string ModuleName => _configurationUnit.Details.ModuleName; - - public string ModuleType => _configurationUnit.Details.ModuleType; - - public string ModuleSource => _configurationUnit.Details.ModuleSource; - - public string ModuleDescription => _configurationUnit.Details.ModuleDescription; - - public string ModuleDocumentationUri => _configurationUnit.Details.ModuleDocumentationUri; - - public string PublishedModuleUri => _configurationUnit.Details.PublishedModuleUri; - - public string Version => _configurationUnit.Details.Version; - - public bool IsLocal => _configurationUnit.Details.IsLocal; - - public string Author => _configurationUnit.Details.Author; - - public string Publisher => _configurationUnit.Details.Publisher; - - public bool IsPublic => _configurationUnit.Details.IsPublic; + public DSCConfigurationUnitViewModel(IDSCUnit configurationUnit) + { + _configurationUnit = configurationUnit; + } private string GetTitle() { @@ -66,11 +46,13 @@ private string GetTitle() return Description; } - if (!string.IsNullOrEmpty(ModuleDescription)) - { - return ModuleDescription; - } - return $"{ModuleName}/{Type}"; } + + [RelayCommand] + private async Task OnLoadedAsync() + { + var result = await _configurationUnit.GetDetailsAsync(); + Details = new DSCConfigurationUnitDetailsViewModel(result); + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/ConfigurationFileView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/ConfigurationFileView.xaml index a63ddfd4b1..15442d1e06 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/ConfigurationFileView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/ConfigurationFileView.xaml @@ -37,6 +37,8 @@ + + @@ -139,6 +141,12 @@ HorizontalContentAlignment="Left" HorizontalAlignment="Stretch"> + + + + + + @@ -190,20 +198,38 @@ + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/ConfigurationFileView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/ConfigurationFileView.xaml.cs index c57122f90f..954bd64121 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/ConfigurationFileView.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/ConfigurationFileView.xaml.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.WinUI; using DevHome.SetupFlow.ViewModels; using Microsoft.UI.Xaml; @@ -42,9 +43,11 @@ private async void Dependency_Click(Hyperlink sender, HyperlinkClickEventArgs ar /// /// Represents a configuration unit data entry. /// -public sealed class ConfigurationUnitDataEntry +public partial class ConfigurationUnitDataEntry : ObservableObject { - public string Key { get; set; } + [ObservableProperty] + private string _key; - public string Value { get; set; } + [ObservableProperty] + private string _value; } From 08652de4171b9937ba154b67217eba7944ea3369 Mon Sep 17 00:00:00 2001 From: Lauren Ciha <64796985+lauren-ciha@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:46:55 -0700 Subject: [PATCH 31/82] Removed extra MoreOptionsButtonName (#3487) --- .../DevHome.Environments/Strings/en-us/Resources.resw | 4 ---- .../DevHome.Environments/ViewModels/ComputeSystemCardBase.cs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw index d820b1ecb3..005de9bf38 100644 --- a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw +++ b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw @@ -343,10 +343,6 @@ Go to extensions library Text for when the environment page in Dev Home is empty and the user has no extensions installed that support environments - - More options - The text narrator should say for the ... button - Sync Accessible name for the button to sync environments. diff --git a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs index a156405a9a..2d004ec56b 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs @@ -69,7 +69,7 @@ public abstract partial class ComputeSystemCardBase : ObservableObject public ComputeSystemCardBase() { - _moreOptionsButtonName = _stringResource.GetLocalized("MoreOptionsButtonName"); + _moreOptionsButtonName = _stringResource.GetLocalized("MoreOptions.AutomationProperties.Name"); } public override string ToString() From b5255970010720c999b5155b7ea041e4f9d71069 Mon Sep 17 00:00:00 2001 From: Branden Bonaby <105318831+bbonaby@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:26:09 -0700 Subject: [PATCH 32/82] Update Windows feature check for environments (#3489) * move code that disables extension is its windows optional feature is absent outside of Compute system service and add new launch button template for when extensions don't provide operations other operations other than launching * change function names * update methods * move enablement to separate methods * remove used usings and add comments * Make sure extension is disabled via local settings * Update common/Helpers/ManagementInfrastructureHelper.cs Co-authored-by: Vineeth Thomas Alex * Update common/Services/IExtensionService.cs Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com> * update based on comments --------- Co-authored-by: Vineeth Thomas Alex Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com> --- .../Helpers/EnvironmentsNotificationHelper.cs | 5 +- .../Helpers/ManagementInfrastructureHelper.cs | 220 +++++++++++------- common/Services/ComputeSystemService.cs | 10 +- common/Services/IExtensionService.cs | 8 + src/Services/ExtensionService.cs | 33 ++- .../BaseSetupFlowTest.cs | 6 +- 6 files changed, 188 insertions(+), 94 deletions(-) diff --git a/common/Environments/Helpers/EnvironmentsNotificationHelper.cs b/common/Environments/Helpers/EnvironmentsNotificationHelper.cs index d9622ce29b..552b9efb8e 100644 --- a/common/Environments/Helpers/EnvironmentsNotificationHelper.cs +++ b/common/Environments/Helpers/EnvironmentsNotificationHelper.cs @@ -19,7 +19,8 @@ using DevHome.Telemetry; using Microsoft.UI.Xaml.Controls; using Microsoft.Windows.DevHome.SDK; -using Serilog; +using Serilog; +using static DevHome.Common.Helpers.ManagementInfrastructureHelper; namespace DevHome.Common.Environments.Helpers; @@ -99,7 +100,7 @@ private void ShowAddUserToAdminGroupAndEnableHyperVNotification() } var userInAdminGroup = _windowsIdentityHelper.IsUserHyperVAdmin(); - var featureEnabled = ManagementInfrastructureHelper.IsWindowsFeatureAvailable(CommonConstants.HyperVWindowsOptionalFeatureName) == FeatureAvailabilityKind.Enabled; + var featureEnabled = IsWindowsOptionalFeatureEnabled(CommonConstants.HyperVWindowsOptionalFeatureName); if (!featureEnabled && !userInAdminGroup) { diff --git a/common/Helpers/ManagementInfrastructureHelper.cs b/common/Helpers/ManagementInfrastructureHelper.cs index 39f20143a4..cfa86254c5 100644 --- a/common/Helpers/ManagementInfrastructureHelper.cs +++ b/common/Helpers/ManagementInfrastructureHelper.cs @@ -1,84 +1,136 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using Microsoft.Management.Infrastructure; -using Serilog; -using static DevHome.Common.Helpers.WindowsOptionalFeatures; - -namespace DevHome.Common.Helpers; - -// States based on InstallState value in Win32_OptionalFeature -// See: https://learn.microsoft.com/windows/win32/cimwin32prov/win32-optionalfeature -public enum FeatureAvailabilityKind -{ - Enabled, - Disabled, - Absent, - Unknown, -} - -public static class ManagementInfrastructureHelper -{ - private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(ManagementInfrastructureHelper)); - - public static FeatureAvailabilityKind IsWindowsFeatureAvailable(string featureName) - { - return GetWindowsFeatureDetails(featureName)?.AvailabilityKind ?? FeatureAvailabilityKind.Unknown; - } - - public static FeatureInfo? GetWindowsFeatureDetails(string featureName) - { - try - { - // use the local session - using var session = CimSession.Create(null); - - // There will only be one feature returned by the query - foreach (var featureInstance in session.QueryInstances("root\\cimv2", "WQL", $"SELECT * FROM Win32_OptionalFeature WHERE Name = '{featureName}'")) - { - if (featureInstance?.CimInstanceProperties["InstallState"].Value is uint installState) - { - var featureAvailability = GetAvailabilityKindFromState(installState); - - _log.Information($"Found feature: '{featureName}' with enablement state: '{featureAvailability}'"); - - // Most optional features do not have a description, so we provide one for known features - var description = featureInstance.CimInstanceProperties["Description"]?.Value as string ?? string.Empty; - if (string.IsNullOrEmpty(description) && WindowsOptionalFeatures.FeatureDescriptions.TryGetValue(featureName, out var featureDescription)) - { - description = featureDescription; - } - - return new FeatureInfo( - featureName, - featureInstance.CimInstanceProperties["Caption"]?.Value as string ?? featureName, - description, - featureAvailability); - } - } - } - catch (Exception ex) - { - _log.Error(ex, $"Error attempting to get the {featureName} feature state"); - } - - _log.Information($"Unable to get state of {featureName} feature"); - return null; - } - - private static FeatureAvailabilityKind GetAvailabilityKindFromState(uint state) - { - switch (state) - { - case 1: - return FeatureAvailabilityKind.Enabled; - case 2: - return FeatureAvailabilityKind.Disabled; - case 3: - return FeatureAvailabilityKind.Absent; - default: - return FeatureAvailabilityKind.Unknown; - } - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Management.Infrastructure; +using Serilog; +using static DevHome.Common.Helpers.CommonConstants; +using static DevHome.Common.Helpers.WindowsOptionalFeatures; + +namespace DevHome.Common.Helpers; + +// States based on InstallState value in Win32_OptionalFeature +// See: https://learn.microsoft.com/windows/win32/cimwin32prov/win32-optionalfeature +public enum FeatureAvailabilityKind +{ + Enabled, + Disabled, + Absent, + Unknown, +} + +public static class ManagementInfrastructureHelper +{ + private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(ManagementInfrastructureHelper)); + + public static readonly Dictionary> ExtensionToFeatureNameMap = new() + { + { HyperVExtensionClassId, new() { HyperVWindowsOptionalFeatureName } }, + }; + + public static FeatureAvailabilityKind GetWindowsFeatureAvailability(string featureName) + { + return GetWindowsFeatureDetails(featureName)?.AvailabilityKind ?? FeatureAvailabilityKind.Unknown; + } + + public static FeatureInfo? GetWindowsFeatureDetails(string featureName) + { + try + { + // use the local session + using var session = CimSession.Create(null); + + // There will only be one feature returned by the query + foreach (var featureInstance in session.QueryInstances("root\\cimv2", "WQL", $"SELECT * FROM Win32_OptionalFeature WHERE Name = '{featureName}'")) + { + if (featureInstance?.CimInstanceProperties["InstallState"].Value is uint installState) + { + var featureAvailability = GetAvailabilityKindFromState(installState); + + _log.Information($"Found feature: '{featureName}' with enablement state: '{featureAvailability}'"); + + // Most optional features do not have a description, so we provide one for known features + var description = featureInstance.CimInstanceProperties["Description"]?.Value as string ?? string.Empty; + if (string.IsNullOrEmpty(description) && WindowsOptionalFeatures.FeatureDescriptions.TryGetValue(featureName, out var featureDescription)) + { + description = featureDescription; + } + + return new FeatureInfo( + featureName, + featureInstance.CimInstanceProperties["Caption"]?.Value as string ?? featureName, + description, + featureAvailability); + } + } + } + catch (Exception ex) + { + _log.Error(ex, $"Error attempting to get the {featureName} feature state"); + } + + _log.Information($"Unable to get state of {featureName} feature"); + return null; + } + + private static FeatureAvailabilityKind GetAvailabilityKindFromState(uint state) + { + switch (state) + { + case 1: + return FeatureAvailabilityKind.Enabled; + case 2: + return FeatureAvailabilityKind.Disabled; + case 3: + return FeatureAvailabilityKind.Absent; + default: + return FeatureAvailabilityKind.Unknown; + } + } + + /// + /// Gets a boolean indicating whether the Windows optional feature that an extension relies on + /// is available on the machine. + /// + /// The name of the Windows optional feature that will be queried + /// True when the feature is either in the enabled or disabled state. False otherwise. + public static bool IsWindowsOptionalFeatureAvailable(string featureName) + { + var availability = GetWindowsFeatureAvailability(featureName); + return (availability == FeatureAvailabilityKind.Enabled) || (availability == FeatureAvailabilityKind.Disabled); + } + + /// + /// Gets a boolean indicating whether the Windows optional feature is enabled. + /// + /// The name of the Windows optional feature that will be queried + /// True only when the optional feature is enabled. False otherwise. + public static bool IsWindowsOptionalFeatureEnabled(string featureName) + { + return GetWindowsFeatureAvailability(featureName) == FeatureAvailabilityKind.Enabled; + } + + /// + /// Gets a boolean indicating whether the Windows optional feature that an extension relies on + /// is available on the machine. + /// + /// The class Id of the out of proc extension object + /// + /// True only when one of the following is met: + /// 1. The classId is not an internal Dev Home extension. + /// 2. The classId is an internal Dev Home extension and the feature is either enabled or disabled. + /// + public static bool IsWindowsOptionalFeatureAvailableForExtension(string extensionClassId) + { + if (ExtensionToFeatureNameMap.TryGetValue(extensionClassId, out var featureList)) + { + return featureList.All(IsWindowsOptionalFeatureAvailable); + } + + // This isn't an internal Dev Home extension that we know about, so we don't know what features it depends on. + // Assume the features are available and let the extension handle this case. + return true; + } +} diff --git a/common/Services/ComputeSystemService.cs b/common/Services/ComputeSystemService.cs index fa5b2a1307..140c50fac0 100644 --- a/common/Services/ComputeSystemService.cs +++ b/common/Services/ComputeSystemService.cs @@ -61,13 +61,11 @@ public async Task> GetComputeSystemProvidersA continue; } - // If we're looking at the Hyper-V extension and the feature isn't present on the users machine, disable the extension. - // This can happen if the user is not on a SKU that supports Hyper-V. - if (extension.ExtensionClassId.Equals(CommonConstants.HyperVExtensionClassId, StringComparison.OrdinalIgnoreCase) && - ManagementInfrastructureHelper.IsWindowsFeatureAvailable(CommonConstants.HyperVWindowsOptionalFeatureName) == FeatureAvailabilityKind.Absent) + // If we're looking at an internal extension that relies on a Windows optional feature being present on the machine. We need to + // check if its on the machine first before allowing it to be added to the list of extensions that are retrieved. If the feature is + // absent or unknown we disable the extension. + if (await _extensionService.DisableExtensionIfWindowsFeatureNotAvailable(extension)) { - _log.Information("User machine does not have the Hyper-V feature present. Disabling the Hyper-V extension"); - _extensionService.DisableExtension(extension.ExtensionUniqueId); continue; } diff --git a/common/Services/IExtensionService.cs b/common/Services/IExtensionService.cs index f1e5796e90..998f3c5dbb 100644 --- a/common/Services/IExtensionService.cs +++ b/common/Services/IExtensionService.cs @@ -27,4 +27,12 @@ public interface IExtensionService public void EnableExtension(string extensionUniqueId); public void DisableExtension(string extensionUniqueId); + + /// + /// Gets a boolean indicating whether the extension was disabled due to the corresponding Windows optional feature + /// being absent from the machine or in an unknown state. + /// + /// The out of proc extension object + /// True only if the extension was disabled. False otherwise. + public Task DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension); } diff --git a/src/Services/ExtensionService.cs b/src/Services/ExtensionService.cs index 032d45c2cf..e38d301cc5 100644 --- a/src/Services/ExtensionService.cs +++ b/src/Services/ExtensionService.cs @@ -9,20 +9,29 @@ using DevHome.Telemetry; using Microsoft.UI.Xaml; using Microsoft.Windows.DevHome.SDK; +using Newtonsoft.Json.Linq; +using Serilog; using Windows.ApplicationModel; using Windows.ApplicationModel.AppExtensions; using Windows.Foundation.Collections; +using YamlDotNet.Core.Tokens; +using static DevHome.Common.Helpers.ManagementInfrastructureHelper; namespace DevHome.Services; public class ExtensionService : IExtensionService, IDisposable { + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ExtensionService)); + public event EventHandler OnExtensionsChanged = (_, _) => { }; private static readonly PackageCatalog _catalog = PackageCatalog.OpenForCurrentUser(); private static readonly object _lock = new(); private readonly SemaphoreSlim _getInstalledExtensionsLock = new(1, 1); private readonly SemaphoreSlim _getInstalledWidgetsLock = new(1, 1); + + private readonly ILocalSettingsService _localSettingsService; + private bool _disposedValue; private const string CreateInstanceProperty = "CreateInstance"; @@ -34,11 +43,12 @@ public class ExtensionService : IExtensionService, IDisposable private static List _installedWidgetsPackageFamilyNames = new(); #pragma warning restore IDE0044 // Add readonly modifier - public ExtensionService() + public ExtensionService(ILocalSettingsService settingsService) { _catalog.PackageInstalling += Catalog_PackageInstalling; _catalog.PackageUninstalling += Catalog_PackageUninstalling; _catalog.PackageUpdating += Catalog_PackageUpdating; + _localSettingsService = settingsService; } private void Catalog_PackageInstalling(PackageCatalog sender, PackageInstallingEventArgs args) @@ -380,4 +390,25 @@ public void DisableExtension(string extensionUniqueId) var extension = _enabledExtensions.Where(extension => extension.ExtensionUniqueId == extensionUniqueId); _enabledExtensions.Remove(extension.First()); } + + /// + public async Task DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension) + { + // Only attempt to disable feature if its available. + if (IsWindowsOptionalFeatureAvailableForExtension(extension.ExtensionClassId)) + { + return false; + } + + _log.Warning($"Disabling extension: '{extension.ExtensionDisplayName}' because its feature is absent or unknown"); + + // Remove extension from list of enabled extensions to prevent Dev Home from re-querying for this extension + // for the rest of its process lifetime. + DisableExtension(extension.ExtensionUniqueId); + + // Update the local settings so the next time the user launches Dev Home the extension will be disabled. + await _localSettingsService.SaveSettingAsync(extension.ExtensionUniqueId + "-ExtensionDisabled", true); + + return true; + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/BaseSetupFlowTest.cs b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/BaseSetupFlowTest.cs index 118ae553d3..b88b11e919 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/BaseSetupFlowTest.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/BaseSetupFlowTest.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using DevHome.Common.Contracts; using DevHome.Common.Services; using DevHome.Contracts.Services; using DevHome.Services; @@ -29,6 +30,8 @@ public class BaseSetupFlowTest protected Mock StringResource { get; private set; } + protected Mock LocalSettingsService { get; private set; } + protected IHost TestHost { get; private set; } #pragma warning restore CS8618 // Non-nullable properties initialized in [TestInitialize] @@ -39,6 +42,7 @@ public void TestInitialize() ThemeSelectorService = new Mock(); RestoreInfo = new Mock(); StringResource = new Mock(); + LocalSettingsService = new Mock(); TestHost = CreateTestHost(); // Configure string resource localization to return the input key by default @@ -60,7 +64,7 @@ private IHost CreateTestHost() services.AddSingleton(ThemeSelectorService!.Object); services.AddSingleton(StringResource.Object); services.AddSingleton(new SetupFlowOrchestrator(null)); - services.AddSingleton(new ExtensionService()); + services.AddSingleton(new ExtensionService(LocalSettingsService.Object)); // App-management view models services.AddTransient(); From 7614791fa1f8a8c06830d3ec5176125220a16044 Mon Sep 17 00:00:00 2001 From: Lauren Ciha <64796985+lauren-ciha@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:28:10 -0700 Subject: [PATCH 33/82] Add narration for close button on Add Repository Dialog (#3491) * Add narration for close button on Add Repo Dialog * Localize AutomationProperties.Name --- .../SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw | 4 ++++ tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml | 1 + 2 files changed, 5 insertions(+) diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw index 9af63eb7b4..938c7eaee2 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -2019,4 +2019,8 @@ Installation Notes Text to introduce installation notes in the summary screen. + + Close + Close button automation name for the login UI dialog + \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml index b90bba2725..62743f4001 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml @@ -148,6 +148,7 @@ HorizontalAlignment="Left" Style="{ThemeResource SubtitleTextBlockStyle}" /> - diff --git a/tools/PI/DevHome.PI/Controls/GlowButton.xaml.cs b/tools/PI/DevHome.PI/Controls/GlowButton.xaml.cs deleted file mode 100644 index 9a416ee19d..0000000000 --- a/tools/PI/DevHome.PI/Controls/GlowButton.xaml.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Windows.Input; -using Microsoft.UI.Composition; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Hosting; - -namespace DevHome.PI.Controls; - -public sealed partial class GlowButton : UserControl -{ -#pragma warning disable CA2211 // Non-constant fields should not be visible -#pragma warning disable SA1401 // Fields should be private - public static DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(GlowButton), new PropertyMetadata(string.Empty)); -#pragma warning restore SA1401 // Fields should be private -#pragma warning restore CA2211 // Non-constant fields should not be visible - - public string Text - { - get => (string)GetValue(TextProperty); - set => SetValue(TextProperty, value); - } - - public ICommand Command - { - get => (ICommand)GetValue(CommandProperty); - set => SetValue(CommandProperty, value); - } - - public static readonly DependencyProperty CommandProperty = - DependencyProperty.Register("Command", typeof(ICommand), typeof(GlowButton), new PropertyMetadata(null)); - - private readonly Compositor compositor; - private readonly ContainerVisual buttonVisual; - private readonly ScalarKeyFrameAnimation opacityAnimation; - - public GlowButton() - { - InitializeComponent(); - compositor = ElementCompositionPreview.GetElementVisual(this).Compositor; - buttonVisual = (ContainerVisual)ElementCompositionPreview.GetElementVisual(this); - - var result = RegisterPropertyChangedCallback(VisibilityProperty, VisibilityChanged); - opacityAnimation = CreatePulseAnimation("Opacity", 0.4f, 1.0f, TimeSpan.FromSeconds(5)); - } - - private ScalarKeyFrameAnimation CreatePulseAnimation(string property, float from, float to, TimeSpan duration) - { - var animation = compositor.CreateScalarKeyFrameAnimation(); - animation.InsertKeyFrame(0.0f, from); - animation.InsertKeyFrame(0.1f, to); - animation.InsertKeyFrame(0.3f, from); - animation.InsertKeyFrame(0.4f, to); - animation.InsertKeyFrame(0.6f, from); - animation.InsertKeyFrame(0.7f, to); - animation.InsertKeyFrame(0.8f, from); - animation.InsertKeyFrame(0.9f, to); - animation.Duration = duration; - animation.Target = property; - return animation; - } - - private void VisibilityChanged(DependencyObject sender, DependencyProperty dp) - { - if (Visibility == Visibility.Visible) - { - buttonVisual.StartAnimation("Opacity", opacityAnimation); - } - } -} diff --git a/tools/PI/DevHome.PI/DevHome.PI.csproj b/tools/PI/DevHome.PI/DevHome.PI.csproj index a606318856..627830c5a0 100644 --- a/tools/PI/DevHome.PI/DevHome.PI.csproj +++ b/tools/PI/DevHome.PI/DevHome.PI.csproj @@ -27,7 +27,6 @@ - @@ -190,9 +189,6 @@ MSBuild:Compile - - MSBuild:Compile - SettingsSingleFileGenerator Settings.Designer.cs diff --git a/tools/PI/DevHome.PI/Helpers/ExpanderBehavior.cs b/tools/PI/DevHome.PI/Helpers/ExpanderBehavior.cs new file mode 100644 index 0000000000..00e3eed6f5 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/ExpanderBehavior.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.PI.Models; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Xaml.Interactivity; + +namespace DevHome.PI.Helpers; + +public class ExpanderBehavior : Behavior +{ + protected override void OnAttached() + { + base.OnAttached(); + AssociatedObject.Expanding += AssociatedObject_Expanding; + AssociatedObject.Collapsed += OnCollapsed; + } + + private void AssociatedObject_Expanding(Expander sender, ExpanderExpandingEventArgs args) + { + if (AssociatedObject.DataContext is Insight insight && !insight.HasBeenRead) + { + insight.HasBeenRead = true; + insight.BadgeOpacity = 0; + } + } + + private void OnCollapsed(Expander sender, ExpanderCollapsedEventArgs args) + { + // Do nothing: once we've set HasBeenRead to true, we don't change it again. + } + + protected override void OnDetaching() + { + base.OnDetaching(); + AssociatedObject.Expanding -= AssociatedObject_Expanding; + AssociatedObject.Collapsed -= OnCollapsed; + } +} diff --git a/tools/PI/DevHome.PI/Models/Insight.cs b/tools/PI/DevHome.PI/Models/Insight.cs index 61e819d355..31310f9ec6 100644 --- a/tools/PI/DevHome.PI/Models/Insight.cs +++ b/tools/PI/DevHome.PI/Models/Insight.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Text.RegularExpressions; +using CommunityToolkit.Mvvm.ComponentModel; namespace DevHome.PI.Models; @@ -16,7 +17,7 @@ internal enum InsightType MemoryViolation, } -public sealed class Insight +public partial class Insight : ObservableObject { internal string Title { get; set; } = string.Empty; @@ -24,7 +25,12 @@ public sealed class Insight internal InsightType InsightType { get; set; } = InsightType.Unknown; - internal bool IsExpanded { get; set; } + [ObservableProperty] + private bool _hasBeenRead; + + // We show the badge by default, as HasBeenRead is false by default. + [ObservableProperty] + private double _badgeOpacity = 1; } internal sealed class InsightRegex diff --git a/tools/PI/DevHome.PI/PIApp.xaml.cs b/tools/PI/DevHome.PI/PIApp.xaml.cs index 2ee3dad5e0..3747bb56c8 100644 --- a/tools/PI/DevHome.PI/PIApp.xaml.cs +++ b/tools/PI/DevHome.PI/PIApp.xaml.cs @@ -54,6 +54,7 @@ public App() // Views and ViewModels services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/tools/PI/DevHome.PI/Pages/InsightsPage.xaml b/tools/PI/DevHome.PI/Pages/InsightsPage.xaml index 81c993a8ca..71757fed5a 100644 --- a/tools/PI/DevHome.PI/Pages/InsightsPage.xaml +++ b/tools/PI/DevHome.PI/Pages/InsightsPage.xaml @@ -5,6 +5,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="using:Microsoft.UI.Xaml.Controls" + xmlns:interactivity="using:Microsoft.Xaml.Interactivity" + xmlns:helpers="using:DevHome.PI.Helpers" xmlns:models="using:DevHome.PI.Models" mc:Ignorable="d" NavigationCacheMode="Enabled"> @@ -20,9 +23,23 @@ - + + + + - + + + + + + diff --git a/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml b/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml index c7384f9e3b..dbc6615020 100644 --- a/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml +++ b/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml @@ -5,8 +5,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls" - xmlns:local="using:DevHome.PI.Controls" + xmlns:toolkit="using:CommunityToolkit.WinUI.UI.Controls" xmlns:models="using:DevHome.PI.Models" xmlns:helpers="using:DevHome.PI.Helpers" mc:Ignorable="d" @@ -44,16 +43,8 @@ BorderThickness="0" Command="{x:Bind ViewModel.ClearWinLogsCommand}"> - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/ViewModels/BarWindowViewModel.cs b/tools/PI/DevHome.PI/ViewModels/BarWindowViewModel.cs index 5182fc73c9..fa6e672d19 100644 --- a/tools/PI/DevHome.PI/ViewModels/BarWindowViewModel.cs +++ b/tools/PI/DevHome.PI/ViewModels/BarWindowViewModel.cs @@ -103,6 +103,12 @@ public partial class BarWindowViewModel : ObservableObject [ObservableProperty] private SizeInt32 _requestedWindowSize; + [ObservableProperty] + private int _unreadInsightsCount; + + [ObservableProperty] + private double _insightsBadgeOpacity; + internal HWND? ApplicationHwnd { get; private set; } public BarWindowViewModel() @@ -400,4 +406,18 @@ public void LaunchAdvancedAppsPageInWindowsSettings() { _ = Launcher.LaunchUriAsync(new("ms-settings:advanced-apps")); } + + public void UpdateUnreadInsightsCount(int count) + { + UnreadInsightsCount = count; + InsightsBadgeOpacity = count > 0 ? 1 : 0; + } + + [RelayCommand] + private void ShowInsightsPage() + { + var barWindow = Application.Current.GetService().DBarWindow; + Debug.Assert(barWindow != null, "BarWindow should not be null."); + barWindow.NavigateTo(typeof(InsightsPageViewModel)); + } } diff --git a/tools/PI/DevHome.PI/ViewModels/InsightsPageViewModel.cs b/tools/PI/DevHome.PI/ViewModels/InsightsPageViewModel.cs index 3ed2572c18..116a95e917 100644 --- a/tools/PI/DevHome.PI/ViewModels/InsightsPageViewModel.cs +++ b/tools/PI/DevHome.PI/ViewModels/InsightsPageViewModel.cs @@ -4,22 +4,31 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; +using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Extensions; using DevHome.PI.Models; +using Microsoft.UI.Xaml; namespace DevHome.PI.ViewModels; public partial class InsightsPageViewModel : ObservableObject { - private Process? targetProcess; + private readonly BarWindowViewModel _barWindowViewModel; + private Process? _targetProcess; [ObservableProperty] - private ObservableCollection insightsList; + private ObservableCollection _insightsList; + + [ObservableProperty] + private int _unreadCount; public InsightsPageViewModel() { + _barWindowViewModel = Application.Current.GetService(); + TargetAppData.Instance.PropertyChanged += TargetApp_PropertyChanged; - insightsList = []; + _insightsList = []; var process = TargetAppData.Instance.TargetProcess; if (process is not null) @@ -30,10 +39,12 @@ public InsightsPageViewModel() public void UpdateTargetProcess(Process process) { - if (targetProcess != process) + if (_targetProcess != process) { - targetProcess = process; + _targetProcess = process; InsightsList.Clear(); + UnreadCount = 0; + _barWindowViewModel.UpdateUnreadInsightsCount(0); } } @@ -50,6 +61,17 @@ private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs internal void AddInsight(Insight insight) { + insight.PropertyChanged += (sender, e) => + { + if (e.PropertyName == nameof(Insight.HasBeenRead)) + { + UnreadCount = InsightsList.Count(insight => !insight.HasBeenRead); + _barWindowViewModel.UpdateUnreadInsightsCount(UnreadCount); + } + }; + + UnreadCount++; + _barWindowViewModel.UpdateUnreadInsightsCount(UnreadCount); InsightsList.Add(insight); } } diff --git a/tools/PI/DevHome.PI/ViewModels/WinLogsPageViewModel.cs b/tools/PI/DevHome.PI/ViewModels/WinLogsPageViewModel.cs index cc66297086..a2ece22bf4 100644 --- a/tools/PI/DevHome.PI/ViewModels/WinLogsPageViewModel.cs +++ b/tools/PI/DevHome.PI/ViewModels/WinLogsPageViewModel.cs @@ -16,6 +16,7 @@ using DevHome.PI.Models; using DevHome.PI.TelemetryEvents; using DevHome.Telemetry; +using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -23,50 +24,46 @@ namespace DevHome.PI.ViewModels; public partial class WinLogsPageViewModel : ObservableObject, IDisposable { - private readonly bool logMeasures; - - private readonly ObservableCollection winLogsOutput; - private readonly Microsoft.UI.Dispatching.DispatcherQueue dispatcher; - - [ObservableProperty] - private ObservableCollection winLogEntries; + private readonly bool _logMeasures; + private readonly ObservableCollection _winLogsOutput; + private readonly DispatcherQueue _dispatcher; [ObservableProperty] - private Visibility insightsButtonVisibility = Visibility.Collapsed; + private ObservableCollection _winLogEntries; [ObservableProperty] - private Visibility runAsAdminVisibility = Visibility.Collapsed; + private Visibility _runAsAdminVisibility = Visibility.Collapsed; [ObservableProperty] - private Visibility gridVisibility = Visibility.Visible; + private Visibility _gridVisibility = Visibility.Visible; [ObservableProperty] - private bool isETWLogsEnabled; + private bool _isETWLogsEnabled; [ObservableProperty] - private bool isDebugOutputEnabled; + private bool _isDebugOutputEnabled; [ObservableProperty] - private bool isEventViewerEnabled = true; + private bool _isEventViewerEnabled = true; [ObservableProperty] - private bool isWEREnabled = true; + private bool _isWEREnabled = true; - private Process? targetProcess; - private WinLogsHelper? winLogsHelper; + private Process? _targetProcess; + private WinLogsHelper? _winLogsHelper; public WinLogsPageViewModel() { // Log feature usage. - logMeasures = true; + _logMeasures = true; App.Log("DevHome.PI_WinLogs_PageInitialize", LogLevel.Measure); - dispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); + _dispatcher = DispatcherQueue.GetForCurrentThread(); TargetAppData.Instance.PropertyChanged += TargetApp_PropertyChanged; - winLogEntries = new(); - winLogsOutput = new(); - winLogsOutput.CollectionChanged += WinLogsOutput_CollectionChanged; + _winLogEntries = []; + _winLogsOutput = []; + _winLogsOutput.CollectionChanged += WinLogsOutput_CollectionChanged; var process = TargetAppData.Instance.TargetProcess; if (process is not null) @@ -77,9 +74,9 @@ public WinLogsPageViewModel() public void UpdateTargetProcess(Process process) { - if (targetProcess != process) + if (_targetProcess != process) { - targetProcess = process; + _targetProcess = process; GridVisibility = Visibility.Visible; RunAsAdminVisibility = Visibility.Collapsed; StopWinLogs(); @@ -89,8 +86,8 @@ public void UpdateTargetProcess(Process process) if (!process.HasExited) { IsETWLogsEnabled = ETWHelper.IsUserInPerformanceLogUsersGroup(); - winLogsHelper = new WinLogsHelper(targetProcess, winLogsOutput); - winLogsHelper.Start(IsETWLogsEnabled, IsDebugOutputEnabled, IsEventViewerEnabled, IsWEREnabled); + _winLogsHelper = new WinLogsHelper(_targetProcess, _winLogsOutput); + _winLogsHelper.Start(IsETWLogsEnabled, IsDebugOutputEnabled, IsEventViewerEnabled, IsWEREnabled); } } catch (Win32Exception ex) @@ -130,7 +127,7 @@ private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs private void StopWinLogs(bool shouldCleanLogs = true) { - winLogsHelper?.Stop(); + _winLogsHelper?.Stop(); if (shouldCleanLogs) { @@ -142,7 +139,7 @@ private void WinLogsOutput_CollectionChanged(object? sender, NotifyCollectionCha { if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null) { - dispatcher.TryEnqueue(() => + _dispatcher.TryEnqueue(() => { foreach (WinLogsEntry newEntry in e.NewItems) { @@ -155,7 +152,7 @@ private void WinLogsOutput_CollectionChanged(object? sender, NotifyCollectionCha public void Dispose() { - winLogsHelper?.Dispose(); + _winLogsHelper?.Dispose(); GC.SuppressFinalize(this); } @@ -166,13 +163,13 @@ public void LogStateChanged(object sender, RoutedEventArgs e) { var isChecked = box.IsChecked; - if (logMeasures) + if (_logMeasures) { App.Log("DevHome.PI_WinLogs_LogStateChanged", LogLevel.Measure, new LogStateChangedEventData(box.Name, (box.IsChecked ?? false) ? "true" : "false"), null); } var tool = (WinLogsTool)box.Tag; - winLogsHelper?.LogStateChanged(tool, isChecked ?? false); + _winLogsHelper?.LogStateChanged(tool, isChecked ?? false); } } @@ -198,55 +195,38 @@ public void UpdateClipboardContent(object sender, DataGridRowClipboardEventArgs private void FindPattern(string message) { var newInsight = InsightsHelper.FindPattern(message); - - dispatcher.TryEnqueue(() => + if (newInsight is not null) { - if (newInsight is not null) + _dispatcher.TryEnqueue(() => { - newInsight.IsExpanded = true; var insightsPageViewModel = Application.Current.GetService(); insightsPageViewModel.AddInsight(newInsight); - InsightsButtonVisibility = Visibility.Visible; - } - else - { - InsightsButtonVisibility = Visibility.Collapsed; - } - }); + }); + } } [RelayCommand] private void ClearWinLogs() { - if (logMeasures) + if (_logMeasures) { // Log feature usage. App.Log("DevHome.PI_WinLogs_ClearLogs", LogLevel.Measure); } - winLogsOutput?.Clear(); - dispatcher.TryEnqueue(() => + _winLogsOutput?.Clear(); + _dispatcher.TryEnqueue(() => { WinLogEntries.Clear(); - - InsightsButtonVisibility = Visibility.Collapsed; }); } - [RelayCommand] - private void ShowInsightsPage() - { - var barWindow = Application.Current.GetService().DBarWindow; - Debug.Assert(barWindow != null, "BarWindow should not be null."); - barWindow.NavigateTo(typeof(InsightsPageViewModel)); - } - [RelayCommand] private void RunAsAdmin() { - if (targetProcess is not null) + if (_targetProcess is not null) { - CommonHelper.RunAsAdmin(targetProcess.Id, nameof(WinLogsPageViewModel)); + CommonHelper.RunAsAdmin(_targetProcess.Id, nameof(WinLogsPageViewModel)); } } } diff --git a/tools/PI/DevHome.PI/Views/BarWindow.cs b/tools/PI/DevHome.PI/Views/BarWindow.cs index bb705f067c..eb2ee73c32 100644 --- a/tools/PI/DevHome.PI/Views/BarWindow.cs +++ b/tools/PI/DevHome.PI/Views/BarWindow.cs @@ -26,7 +26,7 @@ public partial class BarWindow private readonly Settings _settings = Settings.Default; private readonly BarWindowHorizontal _horizontalWindow; private readonly BarWindowVertical _verticalWindow; - private readonly BarWindowViewModel _viewModel = new(); + private readonly BarWindowViewModel _viewModel = Application.Current.GetService(); internal HWND CurrentHwnd { diff --git a/tools/PI/DevHome.PI/Views/BarWindowHorizontal.xaml b/tools/PI/DevHome.PI/Views/BarWindowHorizontal.xaml index 2a878f7c69..f32df515eb 100644 --- a/tools/PI/DevHome.PI/Views/BarWindowHorizontal.xaml +++ b/tools/PI/DevHome.PI/Views/BarWindowHorizontal.xaml @@ -6,7 +6,8 @@ xmlns:winex="using:WinUIEx" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:controls="using:DevHome.PI.Controls" + xmlns:controls="using:Microsoft.UI.Xaml.Controls" + xmlns:local="using:DevHome.PI.Controls" xmlns:helpers="using:DevHome.PI.Helpers" mc:Ignorable="d" Title="" MinHeight="90" MinWidth="700" Height="90" Width="700" MaxHeight="90" @@ -85,11 +86,11 @@ - - @@ -97,14 +98,14 @@ - + - + - + @@ -118,6 +119,24 @@ + + + + @@ -141,7 +160,7 @@ x:Name="LargeContentPanel" Visibility="Collapsed" Grid.Row="2" BorderThickness="0" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"> - + From 63f6c3a256dfdeb3f2af01ded7ed9d1cb16a2913 Mon Sep 17 00:00:00 2001 From: Jason Holmes Date: Thu, 25 Jul 2024 14:28:44 -0700 Subject: [PATCH 35/82] Close the PI process when WM_ENDSESSION is received (#3495) * Enlighten PI to WM_ENDSESSION messages and close the bar window and terminate the process. This will ensure HANG_QUIESE cabs are not collected for Dev Home/PI. * Code review feedback --------- Co-authored-by: Jason Holmes <27746781+jaholme@users.noreply.github.com> --- .../PI/DevHome.PI/Helpers/ServicingHelper.cs | 51 +++++++++++++++++++ .../PI/DevHome.PI/Views/PrimaryWindow.xaml.cs | 11 ++++ 2 files changed, 62 insertions(+) create mode 100644 tools/PI/DevHome.PI/Helpers/ServicingHelper.cs diff --git a/tools/PI/DevHome.PI/Helpers/ServicingHelper.cs b/tools/PI/DevHome.PI/Helpers/ServicingHelper.cs new file mode 100644 index 0000000000..1349aa18e2 --- /dev/null +++ b/tools/PI/DevHome.PI/Helpers/ServicingHelper.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.UI.Xaml; +using Windows.Win32; +using Windows.Win32.Foundation; +using WinUIEx.Messaging; + +namespace DevHome.PI.Helpers; + +// Note: instead of making this class disposable, we're disposing the WindowMessageMonitor in +// UnregisterHotKey, and MainWindow calls this in its Closing event handler. +#pragma warning disable CA1001 // Types that own disposable fields should be disposable +public class ServicingHelper +#pragma warning restore CA1001 // Types that own disposable fields should be disposable +{ + private const string NoWindowHandleException = "Cannot get window handle: are you doing this too early?"; + private readonly HWND _windowHandle; + private readonly WindowMessageMonitor _windowMessageMonitor; + private readonly Action _onSessionEnd; + + public ServicingHelper(Window handlerWindow, Action handleSessionEnd) + { + _onSessionEnd = handleSessionEnd; + + // Set up the window message hook to listen for session end messages. + _windowHandle = (HWND)WinRT.Interop.WindowNative.GetWindowHandle(handlerWindow); + if (_windowHandle.IsNull) + { + throw new InvalidOperationException(NoWindowHandleException); + } + + _windowMessageMonitor = new WindowMessageMonitor(_windowHandle); + _windowMessageMonitor.WindowMessageReceived += OnWindowMessageReceived; + } + + private void OnWindowMessageReceived(object? sender, WindowMessageEventArgs e) + { + if (e.Message.MessageId == PInvoke.WM_ENDSESSION) + { + _onSessionEnd?.Invoke(); + e.Handled = true; + } + } + + internal void Unregister() + { + _windowMessageMonitor?.Dispose(); + } +} diff --git a/tools/PI/DevHome.PI/Views/PrimaryWindow.xaml.cs b/tools/PI/DevHome.PI/Views/PrimaryWindow.xaml.cs index f07803dd2e..fd746907be 100644 --- a/tools/PI/DevHome.PI/Views/PrimaryWindow.xaml.cs +++ b/tools/PI/DevHome.PI/Views/PrimaryWindow.xaml.cs @@ -29,6 +29,8 @@ public sealed partial class PrimaryWindow : WindowEx private HotKeyHelper? _hotKeyHelper; + private ServicingHelper? _servicingHelper; + public BarWindow? DBarWindow { get; private set; } public PrimaryWindow() @@ -62,6 +64,7 @@ public void ClearBarWindow() private void Window_Loaded(object sender, RoutedEventArgs e) { App.Log("DevHome.PI_MainWindows_Loaded", LogLevel.Measure); + _servicingHelper = new(this, HandleSessionEnd); _hotKeyHelper = new(this, HandleHotKey); _hotKeyHelper.RegisterHotKey(HotKey, KeyModifier); } @@ -70,6 +73,14 @@ private void WindowEx_Closed(object sender, WindowEventArgs args) { DBarWindow?.Close(); _hotKeyHelper?.UnregisterHotKey(); + _servicingHelper?.Unregister(); + } + + public void HandleSessionEnd() + { + App.Log("DevHome.PI_SessionEnd", LogLevel.Info); + DBarWindow?.Close(); + Process.GetCurrentProcess().Kill(); } public void HandleHotKey(int keyId) From ededd2ce8cc27d37f8f91e6d66ba3423342994b9 Mon Sep 17 00:00:00 2001 From: andreww-msft <30507740+andreww-msft@users.noreply.github.com> Date: Fri, 26 Jul 2024 09:57:57 -0700 Subject: [PATCH 36/82] Removed non-navigation items from the Navigation panel (#3501) --- .../Controls/ExpandedViewControl.xaml | 228 ++++++++---------- .../Controls/ExpandedViewControl.xaml.cs | 14 +- tools/PI/DevHome.PI/Pages/AppDetailsPage.xaml | 19 +- .../Services/PINavigationService.cs | 2 +- .../DevHome.PI/Strings/en-us/Resources.resw | 4 - .../ViewModels/AppDetailsPageViewModel.cs | 19 ++ .../ExpandedViewControlViewModel.cs | 130 +++++----- 7 files changed, 200 insertions(+), 216 deletions(-) diff --git a/tools/PI/DevHome.PI/Controls/ExpandedViewControl.xaml b/tools/PI/DevHome.PI/Controls/ExpandedViewControl.xaml index a3abe6387c..434bc461a6 100644 --- a/tools/PI/DevHome.PI/Controls/ExpandedViewControl.xaml +++ b/tools/PI/DevHome.PI/Controls/ExpandedViewControl.xaml @@ -24,147 +24,129 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/tools/PI/DevHome.PI/Controls/ExpandedViewControl.xaml.cs b/tools/PI/DevHome.PI/Controls/ExpandedViewControl.xaml.cs index 1379168140..c4be4e6562 100644 --- a/tools/PI/DevHome.PI/Controls/ExpandedViewControl.xaml.cs +++ b/tools/PI/DevHome.PI/Controls/ExpandedViewControl.xaml.cs @@ -3,7 +3,6 @@ using System; using DevHome.PI.ViewModels; -using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; @@ -11,12 +10,12 @@ namespace DevHome.PI.Controls; public sealed partial class ExpandedViewControl : UserControl { - private readonly ExpandedViewControlViewModel viewModel = new(); + private readonly ExpandedViewControlViewModel _viewModel = new(); public ExpandedViewControl() { InitializeComponent(); - viewModel.NavigationService.Frame = PageFrame; + _viewModel.NavigationService.Frame = PageFrame; } public Frame GetPageFrame() @@ -26,17 +25,12 @@ public Frame GetPageFrame() public void NavigateTo(Type viewModelType) { - viewModel.NavigateTo(viewModelType); - } - - private void SettingsButton_Click(object sender, RoutedEventArgs e) - { - NavigateToSettings(typeof(SettingsPageViewModel).FullName!); + _viewModel.NavigateTo(viewModelType); } public void NavigateToSettings(string viewModelType) { - viewModel.NavigateToSettings(viewModelType); + _viewModel.NavigateToSettings(viewModelType); } private void GridSplitter_PointerPressed(object sender, PointerRoutedEventArgs e) diff --git a/tools/PI/DevHome.PI/Pages/AppDetailsPage.xaml b/tools/PI/DevHome.PI/Pages/AppDetailsPage.xaml index 07f816efa3..cc81345b4d 100644 --- a/tools/PI/DevHome.PI/Pages/AppDetailsPage.xaml +++ b/tools/PI/DevHome.PI/Pages/AppDetailsPage.xaml @@ -28,9 +28,24 @@ - + + + + + + + + - + diff --git a/tools/PI/DevHome.PI/Services/PINavigationService.cs b/tools/PI/DevHome.PI/Services/PINavigationService.cs index 4f9aff96bc..180a66cfaa 100644 --- a/tools/PI/DevHome.PI/Services/PINavigationService.cs +++ b/tools/PI/DevHome.PI/Services/PINavigationService.cs @@ -161,6 +161,6 @@ private void OnNavigated(object sender, NavigationEventArgs e) public static object? GetPageViewModel(Frame frame) { - return frame.Content?.GetType().GetProperty("viewModel")?.GetValue(frame.Content, null); + return frame.Content?.GetType().GetProperty("_viewModel")?.GetValue(frame.Content, null); } } diff --git a/tools/PI/DevHome.PI/Strings/en-us/Resources.resw b/tools/PI/DevHome.PI/Strings/en-us/Resources.resw index afc974fcef..9eeebf7d64 100644 --- a/tools/PI/DevHome.PI/Strings/en-us/Resources.resw +++ b/tools/PI/DevHome.PI/Strings/en-us/Resources.resw @@ -917,10 +917,6 @@ Enable App Execution Alias for Project Ironsides and try again {Locked="Ironsides"} Content Dialog title to enable app execution alias for Project Ironsides - - Detach from target app - Button in the expanded view to detach from the target app - Detach from the currently targeted process Used to describe what the DetachAppButton does diff --git a/tools/PI/DevHome.PI/ViewModels/AppDetailsPageViewModel.cs b/tools/PI/DevHome.PI/ViewModels/AppDetailsPageViewModel.cs index 4e1d33f5e0..710d152ccc 100644 --- a/tools/PI/DevHome.PI/ViewModels/AppDetailsPageViewModel.cs +++ b/tools/PI/DevHome.PI/ViewModels/AppDetailsPageViewModel.cs @@ -38,6 +38,9 @@ public partial class AppDetailsPageViewModel : ObservableObject [ObservableProperty] private Visibility _processPackageVisibility = Visibility.Collapsed; + [ObservableProperty] + private Visibility _appSettingsVisibility = Visibility.Collapsed; + private Process? _targetProcess; public AppDetailsPageViewModel() @@ -48,8 +51,13 @@ public AppDetailsPageViewModel() var process = TargetAppData.Instance.TargetProcess; if (process is not null) { + AppSettingsVisibility = Visibility.Visible; UpdateTargetProcess(process); } + else + { + AppSettingsVisibility = Visibility.Collapsed; + } } public void UpdateTargetProcess(Process process) @@ -123,8 +131,13 @@ private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs { if (TargetAppData.Instance.TargetProcess is not null) { + AppSettingsVisibility = Visibility.Visible; UpdateTargetProcess(TargetAppData.Instance.TargetProcess); } + else + { + AppSettingsVisibility = Visibility.Collapsed; + } } } @@ -137,6 +150,12 @@ private void RunAsAdmin() } } + [RelayCommand] + public void DetachFromProcess() + { + TargetAppData.Instance.ClearAppData(); + } + private void GetPackageInfo(ProcessDiagnosticInfo pdi) { if (!pdi.IsPackaged) diff --git a/tools/PI/DevHome.PI/ViewModels/ExpandedViewControlViewModel.cs b/tools/PI/DevHome.PI/ViewModels/ExpandedViewControlViewModel.cs index 077b47631c..4cfc47f6c4 100644 --- a/tools/PI/DevHome.PI/ViewModels/ExpandedViewControlViewModel.cs +++ b/tools/PI/DevHome.PI/ViewModels/ExpandedViewControlViewModel.cs @@ -13,93 +13,85 @@ using DevHome.PI.Models; using DevHome.PI.Properties; using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; namespace DevHome.PI.ViewModels; public partial class ExpandedViewControlViewModel : ObservableObject { - private readonly Microsoft.UI.Dispatching.DispatcherQueue dispatcher; + private readonly Microsoft.UI.Dispatching.DispatcherQueue _dispatcher; [ObservableProperty] - private Visibility perfMarkersVisibility = Visibility.Collapsed; + private Visibility _perfMarkersVisibility = Visibility.Collapsed; [ObservableProperty] - private string applicationPid = string.Empty; + private string _applicationPid = string.Empty; [ObservableProperty] - private string applicationName = string.Empty; + private string _applicationName = string.Empty; [ObservableProperty] - private string cpuUsage = string.Empty; + private string _cpuUsage = string.Empty; [ObservableProperty] - private string ramUsage = string.Empty; + private string _ramUsage = string.Empty; [ObservableProperty] - private string diskUsage = string.Empty; + private string _diskUsage = string.Empty; [ObservableProperty] - private string clipboardContentsHex = string.Empty; + private string _clipboardContentsHex = string.Empty; [ObservableProperty] - private string clipboardContentsDec = string.Empty; + private string _clipboardContentsDec = string.Empty; [ObservableProperty] - private string clipboardContentsCode = string.Empty; + private string _clipboardContentsCode = string.Empty; [ObservableProperty] - private string clipboardContentsHelp = string.Empty; + private string _clipboardContentsHelp = string.Empty; [ObservableProperty] - private string title = string.Empty; + private string _title = string.Empty; [ObservableProperty] - private string settingsHeader = string.Empty; + private ObservableCollection _links; [ObservableProperty] - private ObservableCollection links; - - [ObservableProperty] - private int selectedNavLinkIndex = 0; - - [ObservableProperty] - private Visibility appSettingsVisibility = Visibility.Collapsed; + private int _selectedNavLinkIndex = 0; [ObservableProperty] private bool _applyAppFiltering; public INavigationService NavigationService { get; } - private readonly PageNavLink appDetailsNavLink; - private readonly PageNavLink resourceUsageNavLink; - private readonly PageNavLink modulesNavLink; - private readonly PageNavLink werNavLink; - private readonly PageNavLink winLogsNavLink; - private readonly PageNavLink processListNavLink; - private readonly PageNavLink insightsNavLink; + private readonly PageNavLink _appDetailsNavLink; + private readonly PageNavLink _resourceUsageNavLink; + private readonly PageNavLink _modulesNavLink; + private readonly PageNavLink _werNavLink; + private readonly PageNavLink _winLogsNavLink; + private readonly PageNavLink _processListNavLink; + private readonly PageNavLink _insightsNavLink; + private readonly PageNavLink _settingsNavLink; public ExpandedViewControlViewModel() { - dispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); + _dispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); TargetAppData.Instance.PropertyChanged += TargetApp_PropertyChanged; PerfCounters.Instance.PropertyChanged += PerfCounterHelper_PropertyChanged; ClipboardMonitor.Instance.PropertyChanged += Clipboard_PropertyChanged; - appDetailsNavLink = new PageNavLink("\uE71D", CommonHelper.GetLocalizedString("AppDetailsTextBlock/Text"), typeof(AppDetailsPageViewModel)); - resourceUsageNavLink = new PageNavLink("\uE950", CommonHelper.GetLocalizedString("ResourceUsageHeaderTextBlock/Text"), typeof(ResourceUsagePageViewModel)); - modulesNavLink = new PageNavLink("\uE74C", CommonHelper.GetLocalizedString("ModulesHeaderTextBlock/Text"), typeof(ModulesPageViewModel)); - werNavLink = new PageNavLink("\uE7BA", CommonHelper.GetLocalizedString("WERHeaderTextBlock/Text"), typeof(WERPageViewModel)); - winLogsNavLink = new PageNavLink("\uE7C4", CommonHelper.GetLocalizedString("WinLogsHeaderTextBlock/Text"), typeof(WinLogsPageViewModel)); - processListNavLink = new PageNavLink("\uE8FD", CommonHelper.GetLocalizedString("ProcessListHeaderTextBlock/Text"), typeof(ProcessListPageViewModel)); - insightsNavLink = new PageNavLink("\uE946", CommonHelper.GetLocalizedString("InsightsHeaderTextBlock/Text"), typeof(InsightsPageViewModel)); + _appDetailsNavLink = new PageNavLink("\uE71D", CommonHelper.GetLocalizedString("AppDetailsTextBlock/Text"), typeof(AppDetailsPageViewModel)); + _resourceUsageNavLink = new PageNavLink("\uE950", CommonHelper.GetLocalizedString("ResourceUsageHeaderTextBlock/Text"), typeof(ResourceUsagePageViewModel)); + _modulesNavLink = new PageNavLink("\uE74C", CommonHelper.GetLocalizedString("ModulesHeaderTextBlock/Text"), typeof(ModulesPageViewModel)); + _werNavLink = new PageNavLink("\uE7BA", CommonHelper.GetLocalizedString("WERHeaderTextBlock/Text"), typeof(WERPageViewModel)); + _winLogsNavLink = new PageNavLink("\uE7C4", CommonHelper.GetLocalizedString("WinLogsHeaderTextBlock/Text"), typeof(WinLogsPageViewModel)); + _processListNavLink = new PageNavLink("\uE8FD", CommonHelper.GetLocalizedString("ProcessListHeaderTextBlock/Text"), typeof(ProcessListPageViewModel)); + _insightsNavLink = new PageNavLink("\uE946", CommonHelper.GetLocalizedString("InsightsHeaderTextBlock/Text"), typeof(InsightsPageViewModel)); + _settingsNavLink = new PageNavLink("\uE713", CommonHelper.GetLocalizedString("SettingsToolHeaderTextBlock/Text"), typeof(SettingsPageViewModel)); - links = new(); + _links = []; ApplyAppFiltering = Settings.Default.ApplyAppFilteringToData; - - appSettingsVisibility = TargetAppData.Instance.TargetProcess is not null ? Visibility.Visible : Visibility.Collapsed; - AddPagesIfNecessary(TargetAppData.Instance.TargetProcess); // Initial values @@ -107,29 +99,27 @@ public ExpandedViewControlViewModel() RamUsage = CommonHelper.GetLocalizedString("MemoryPerfTextFormat", PerfCounters.Instance.RamUsageInMB); DiskUsage = CommonHelper.GetLocalizedString("DiskPerfTextFormat", PerfCounters.Instance.DiskUsage); NavigationService = Application.Current.GetService(); - - SettingsHeader = CommonHelper.GetLocalizedString("SettingsToolHeaderTextBlock/Text"); } private void PerfCounterHelper_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(PerfCounters.CpuUsage)) { - dispatcher.TryEnqueue(() => + _dispatcher.TryEnqueue(() => { CpuUsage = CommonHelper.GetLocalizedString("CpuPerfTextFormat", PerfCounters.Instance.CpuUsage); }); } else if (e.PropertyName == nameof(PerfCounters.RamUsageInMB)) { - dispatcher.TryEnqueue(() => + _dispatcher.TryEnqueue(() => { RamUsage = CommonHelper.GetLocalizedString("MemoryPerfTextFormat", PerfCounters.Instance.RamUsageInMB); }); } else if (e.PropertyName == nameof(PerfCounters.DiskUsage)) { - dispatcher.TryEnqueue(() => + _dispatcher.TryEnqueue(() => { DiskUsage = CommonHelper.GetLocalizedString("DiskPerfTextFormat", PerfCounters.Instance.DiskUsage); }); @@ -142,7 +132,7 @@ private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs { var process = TargetAppData.Instance.TargetProcess; - dispatcher.TryEnqueue(() => + _dispatcher.TryEnqueue(() => { // The App status bar is only visibile if we're attached to a process PerfMarkersVisibility = process is null ? Visibility.Collapsed : Visibility.Visible; @@ -152,8 +142,6 @@ private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs ApplicationName = process?.ProcessName ?? string.Empty; Title = process?.ProcessName ?? string.Empty; - AppSettingsVisibility = process is not null ? Visibility.Visible : Visibility.Collapsed; - if (process is null) { RemoveAppSpecificPages(); @@ -168,7 +156,7 @@ private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs { var newAppName = TargetAppData.Instance.AppName; - dispatcher.TryEnqueue(() => + _dispatcher.TryEnqueue(() => { ApplicationName = newAppName; Title = newAppName; @@ -182,7 +170,7 @@ private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs var process = TargetAppData.Instance.TargetProcess; if (process != null && process.HasExited) { - dispatcher.TryEnqueue(() => + _dispatcher.TryEnqueue(() => { Title = CommonHelper.GetLocalizedString("TerminatedText", ApplicationName); }); @@ -193,7 +181,7 @@ private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs private void Clipboard_PropertyChanged(object? sender, PropertyChangedEventArgs e) { var clipboardContents = ClipboardMonitor.Instance.Contents; - dispatcher.TryEnqueue(() => + _dispatcher.TryEnqueue(() => { ClipboardContentsHex = clipboardContents.Hex; ClipboardContentsDec = clipboardContents.Dec; @@ -204,25 +192,26 @@ private void Clipboard_PropertyChanged(object? sender, PropertyChangedEventArgs private void AddPagesIfNecessary(Process? process) { - if (!Links.Contains(processListNavLink)) + if (!Links.Contains(_processListNavLink)) { - Links.Add(processListNavLink); - Links.Add(werNavLink); - Links.Add(insightsNavLink); + Links.Add(_processListNavLink); + Links.Add(_werNavLink); + Links.Add(_insightsNavLink); + Links.Add(_settingsNavLink); } // If App Details is missing, add all other pages. - if (!Links.Contains(appDetailsNavLink)) + if (!Links.Contains(_appDetailsNavLink)) { if (process is not null) { - Links.Insert(0, appDetailsNavLink); - Links.Insert(1, resourceUsageNavLink); - Links.Insert(2, modulesNavLink); + Links.Insert(0, _appDetailsNavLink); + Links.Insert(1, _resourceUsageNavLink); + Links.Insert(2, _modulesNavLink); // Process List #3 // WER #4 - Links.Insert(5, winLogsNavLink); + Links.Insert(5, _winLogsNavLink); // Insights #6; } @@ -232,12 +221,12 @@ private void AddPagesIfNecessary(Process? process) private void RemoveAppSpecificPages() { // First navigate to ProcessListPage, then remove all other pages. - SelectedNavLinkIndex = Links.IndexOf(processListNavLink); + SelectedNavLinkIndex = Links.IndexOf(_processListNavLink); - Links.Remove(appDetailsNavLink); - Links.Remove(resourceUsageNavLink); - Links.Remove(modulesNavLink); - Links.Remove(winLogsNavLink); + Links.Remove(_appDetailsNavLink); + Links.Remove(_resourceUsageNavLink); + Links.Remove(_modulesNavLink); + Links.Remove(_winLogsNavLink); } public void NavigateTo(Type viewModelType) @@ -264,11 +253,6 @@ public void Navigate() public void NavigateToSettings(string viewModelType) { - // Because the Settings item isn't part of our NavLink list, when the user selects Settings, - // we need to move the list selection so that when they subsequently select an item from - // the NavLinks, we'll navigate to the correct page even if that was the previously-selected item. - SelectedNavLinkIndex = -1; - var navigationService = Application.Current.GetService(); var mainSettingsPage = typeof(SettingsPageViewModel).FullName!; navigationService.NavigateTo(mainSettingsPage); @@ -278,12 +262,6 @@ public void NavigateToSettings(string viewModelType) } } - [RelayCommand] - public void DetachFromProcess() - { - TargetAppData.Instance.ClearAppData(); - } - [RelayCommand] public void ApplyAppFilteringToData() { From 48e8dd49234a46cda8b6ca0d3fa2d144b9556ca9 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Fri, 26 Jul 2024 10:18:41 -0700 Subject: [PATCH 37/82] [Environments] Adding configuration flow (#3492) * Framework * Added next page logic * Added review item to next page * Fixed missing ComputeSystem * PR comment changes I * Added background thread --------- Co-authored-by: Huzaifa Danish --- .../Helpers/DataExtractor.cs | 9 ++++- .../Strings/en-us/Resources.resw | 4 ++ .../ViewModels/ComputeSystemViewModel.cs | 6 ++- .../CreateComputeSystemOperationViewModel.cs | 1 + .../ViewModels/LandingPageViewModel.cs | 13 +++++++ .../ViewModels/OperationsViewModel.cs | 33 ++++++++++++++-- .../Models/ConfigureTargetTask.cs | 1 + .../TaskGroups/SetupTargetTaskGroup.cs | 1 + .../ViewModels/MainPageViewModel.cs | 39 ++++++++++++++++++- .../ViewModels/SetupFlowViewModel.cs | 29 ++++++++++++-- 10 files changed, 127 insertions(+), 9 deletions(-) diff --git a/tools/Environments/DevHome.Environments/Helpers/DataExtractor.cs b/tools/Environments/DevHome.Environments/Helpers/DataExtractor.cs index 4704f599b5..698866c28b 100644 --- a/tools/Environments/DevHome.Environments/Helpers/DataExtractor.cs +++ b/tools/Environments/DevHome.Environments/Helpers/DataExtractor.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Threading.Tasks; using DevHome.Common.Environments.Models; @@ -98,7 +99,7 @@ public static async Task> FillDotButtonPinOperationsAsync /// Returns the list of operations to be added to the launch button. /// // Compute system used to fill OperationsViewModel's callback function. - public static List FillLaunchButtonOperations(ComputeSystemCache computeSystem) + public static List FillLaunchButtonOperations(ComputeSystemProvider provider, ComputeSystemCache computeSystem, Action? configurationCallback) { var operations = new List(); var supportedOperations = computeSystem.SupportedOperations.Value; @@ -151,6 +152,12 @@ public static List FillLaunchButtonOperations(ComputeSystem _stringResource.GetLocalized("Operations_Terminate"), "\uEE95", computeSystem.TerminateAsync, ComputeSystemOperations.Terminate)); } + if (supportedOperations.HasFlag(ComputeSystemOperations.ApplyConfiguration) && configurationCallback is not null) + { + operations.Add(new OperationsViewModel( + _stringResource.GetLocalized("Operations_ApplyConfiguration"), "\uE835", configurationCallback, provider, computeSystem)); + } + return operations; } } diff --git a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw index 005de9bf38..d43afe0231 100644 --- a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw +++ b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw @@ -355,4 +355,8 @@ More options Name of the button that brings up the "More options" menu + + Set up + Value to be shown in the button option to start the configuration flow + \ No newline at end of file diff --git a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs index 8dfe018f26..9d296e476d 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs @@ -58,6 +58,8 @@ public partial class ComputeSystemViewModel : ComputeSystemCardBase, IRecipient< private bool _disposedValue; + private Action? _configurationAction; + /// /// Initializes a new instance of the class. /// This class requires a 3-step initialization: @@ -73,6 +75,7 @@ public ComputeSystemViewModel( IComputeSystem system, ComputeSystemProvider provider, Func removalAction, + Action? configurationAction, string packageFullName, Window window) { @@ -83,6 +86,7 @@ public ComputeSystemViewModel( ComputeSystem = new(system); PackageFullName = packageFullName; _removalAction = removalAction; + _configurationAction = configurationAction; _stringResource = new StringResource("DevHome.Environments.pri", "DevHome.Environments/Resources"); } @@ -133,7 +137,7 @@ private async Task InitializeOperationDataAsync() ShouldShowDotOperations = false; ShouldShowSplitButton = false; - RegisterForAllOperationMessages(DataExtractor.FillDotButtonOperations(ComputeSystem, _mainWindow), DataExtractor.FillLaunchButtonOperations(ComputeSystem)); + RegisterForAllOperationMessages(DataExtractor.FillDotButtonOperations(ComputeSystem, _mainWindow), DataExtractor.FillLaunchButtonOperations(_provider, ComputeSystem, _configurationAction)); _ = Task.Run(async () => { diff --git a/tools/Environments/DevHome.Environments/ViewModels/CreateComputeSystemOperationViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/CreateComputeSystemOperationViewModel.cs index b64cf11db0..d988677e63 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/CreateComputeSystemOperationViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/CreateComputeSystemOperationViewModel.cs @@ -160,6 +160,7 @@ private async void AddComputeSystemToUI(CreateComputeSystemResult result) result.ComputeSystem, Operation.ProviderDetails.ComputeSystemProvider, _removalAction, + null, Operation.ProviderDetails.ExtensionWrapper.PackageFullName, _mainWindow); diff --git a/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs index 5fa8022064..87ca6b37f4 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs @@ -152,6 +152,18 @@ public void CallToActionInvokeButton() _navigationService.NavigateTo(KnownPageKeys.SetupFlow, "startCreationFlow;EnvironmentsLandingPage"); } + public void ConfigureComputeSystem(ComputeSystemReviewItem item) + { + _log.Information("User clicked on the setup button. Navigating to the Setup an Environment page in Setup flow"); + object[] parameters = { "StartConfigurationFlow", "EnvironmentsLandingPage", item }; + + // Run on the UI thread + _mainWindow.DispatcherQueue.EnqueueAsync(() => + { + _navigationService.NavigateTo(KnownPageKeys.SetupFlow, parameters); + }); + } + // Updates the last sync time on the UI thread after set delay private async Task UpdateLastSyncTimeUI(string time, TimeSpan delay, CancellationToken token) { @@ -337,6 +349,7 @@ private async Task AddAllComputeSystemsFromAProvider(ComputeSystemsLoadedData da computeSystem, provider, RemoveComputeSystemCard, + ConfigureComputeSystem, packageFullName, _mainWindow); diff --git a/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs index 7c25a61a13..6bf6a2920d 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; +using DevHome.Common.Environments.Models; using DevHome.Common.Services; using DevHome.Environments.Models; using Microsoft.UI.Xaml; @@ -37,6 +38,10 @@ public partial class OperationsViewModel : IEquatable private readonly string _additionalContext = string.Empty; + private readonly Window? _mainWindow; + + private readonly StringResource _stringResource = new("DevHome.Environments.pri", "DevHome.Environments/Resources"); + public string Name { get; } public ComputeSystemOperations ComputeSystemOperation { get; } @@ -47,9 +52,9 @@ public partial class OperationsViewModel : IEquatable private Action? DevHomeAction { get; } - private readonly Window? _mainWindow; + private Action? DevHomeActionWithReviewItem { get; } - private readonly StringResource _stringResource = new("DevHome.Environments.pri", "DevHome.Environments/Resources"); + private ComputeSystemReviewItem? _item; public OperationsViewModel( string name, @@ -89,6 +94,20 @@ public OperationsViewModel(string name, string icon, Action command) DevHomeAction = command; } + public OperationsViewModel( + string name, + string icon, + Action? command, + ComputeSystemProvider provider, + ComputeSystemCache cache) + { + _operationKind = OperationKind.DevHomeAction; + Name = name; + IconGlyph = icon; + DevHomeActionWithReviewItem = command; + _item = new(cache, provider); + } + private void RunAction() { // To Do: Need to disable the card UI while the operation is in progress and handle failures. @@ -96,7 +115,15 @@ private void RunAction() { if (_operationKind == OperationKind.DevHomeAction) { - DevHomeAction!(); + if (DevHomeAction != null) + { + DevHomeAction(); + } + else if (DevHomeActionWithReviewItem != null && _item != null) + { + DevHomeActionWithReviewItem(_item); + } + return; } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs index 2e7f3ec3fe..bb9fbf0cf7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using CommunityToolkit.WinUI; +using DevHome.Common.Environments.Models; using DevHome.Common.Environments.Services; using DevHome.Common.Extensions; using DevHome.Common.Services; diff --git a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs index 7bef0ace60..fc492807d6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs @@ -13,6 +13,7 @@ namespace DevHome.SetupFlow.TaskGroups; public class SetupTargetTaskGroup : ISetupTaskGroup { private readonly SetupTargetViewModel _setupTargetViewModel; + private readonly SetupTargetReviewViewModel _setupTargetReviewViewModel; private readonly ConfigureTargetTask _setupTargetTaskGroup; diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs index 0c1e49042b..e204853b68 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs @@ -7,6 +7,8 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Environments.Models; +using DevHome.Common.Environments.Services; using DevHome.Common.Extensions; using DevHome.Common.Models; using DevHome.Common.Services; @@ -42,6 +44,7 @@ public partial class MainPageViewModel : SetupPageViewModelBase, IDisposable private readonly IWinGet _winget; private readonly IDSC _dsc; private readonly IExperimentationService _experimentationService; + private readonly IComputeSystemManager _computeSystemManager; public MainPageBannerViewModel BannerViewModel { get; } @@ -80,13 +83,15 @@ public MainPageViewModel( IDSC dsc, IHost host, MainPageBannerViewModel bannerViewModel, - IExperimentationService experimentationService) + IExperimentationService experimentationService, + IComputeSystemManager computeSystemManager) : base(stringResource, orchestrator) { _host = host; _winget = winget; _dsc = dsc; _experimentationService = experimentationService; + _computeSystemManager = computeSystemManager; IsNavigationBarVisible = false; IsStepPage = false; @@ -226,6 +231,38 @@ private void StartSetupForTargetEnvironment(string flowTitle) _host.GetService()); } + /// + /// Starts the setup target flow from the environments page. + /// + public void StartSetupForTargetEnvironmentWithTelemetry(string flowTitle, string navigationAction, string originPage, ComputeSystemReviewItem item) + { + var setupTask = _host.GetService(); + + _log.Information("Starting setup for target environment from the Environments page"); + StartSetupFlowForTaskGroups( + flowTitle, + "SetupTargetEnvironment", + setupTask, + _host.GetService(), + _host.GetService()); + + TelemetryFactory.Get().Log( + "Setup_Environment_button_Clicked", + LogLevel.Measure, + new EnvironmentRedirectionUserEvent(navigationAction: navigationAction, originPage), + relatedActivityId: Orchestrator.ActivityId); + + Orchestrator.GoToNextPage().GetAwaiter().GetResult(); + + // We add the target environment to the setup task after because the constructor flow + // of the setup task group sets the target environment to null on the main thread. + // We move it to a background thread so that there isn't a race condition with the main thread. + Task.Run(() => + { + _computeSystemManager.ComputeSystemSetupItem = item; + }); + } + /// /// Starts a setup flow that only includes repo config. /// diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs index ac5b000635..57c548d2de 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Environments.Models; using DevHome.Common.Extensions; using DevHome.Common.Services; using DevHome.Common.TelemetryEvents.SetupFlow; @@ -29,6 +30,8 @@ public partial class SetupFlowViewModel : ObservableObject private readonly string _creationFlowNavigationParameter = "StartCreationFlow"; + private readonly string _configurationFlowNavigationParameter = "StartConfigurationFlow"; + public SetupFlowOrchestrator Orchestrator { get; } public event EventHandler EndSetupFlow = (s, e) => { }; @@ -123,7 +126,7 @@ public async Task StartFileActivationFlowAsync(StorageFile file) await _mainPageViewModel.StartConfigurationFileAsync(file); } - public void StartCreationFlowAsync(string originPage) + public void StartCreationFlow(string originPage) { Orchestrator.FlowPages = [_mainPageViewModel]; @@ -131,9 +134,17 @@ public void StartCreationFlowAsync(string originPage) _mainPageViewModel.StartCreateEnvironmentWithTelemetry(string.Empty, _creationFlowNavigationParameter, originPage); } + public void StartSetupFlow(string originPage, ComputeSystemReviewItem item) + { + Orchestrator.FlowPages = [_mainPageViewModel]; + + // This method is only called when the user clicks a button that redirects them to 'Setup' flow in the Environments page. + _mainPageViewModel.StartSetupForTargetEnvironmentWithTelemetry(string.Empty, _configurationFlowNavigationParameter, originPage, item); + } + public void OnNavigatedTo(NavigationEventArgs args) { - // The setup flow isn't setup to support using the navigation service to navigate to specific + // The setup flow isn't set up 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(); @@ -146,7 +157,19 @@ public void OnNavigatedTo(NavigationEventArgs args) // and the second value being the page name that redirection came from for telemetry purposes. var parameters = parameter.Split(';'); Cancel(); - StartCreationFlowAsync(originPage: parameters[1]); + StartCreationFlow(originPage: parameters[1]); + } + else if (args.Parameter is object[] configObjs && configObjs.Length == 3) + { + if (configObjs[0] is string configObj && configObj.Equals(_configurationFlowNavigationParameter, StringComparison.OrdinalIgnoreCase)) + { + // We expect that when navigating from anywhere in Dev Home to the create environment page + // that the arg.Parameter variable be an object array with the the first value being 'StartCreationFlow', + // the second value being the page name that redirection came from for telemetry purposes, and + // the third value being the ComputeSystemReviewItem to setup. + Cancel(); + StartSetupFlow(originPage: configObjs[1] as string, item: configObjs[2] as ComputeSystemReviewItem); + } } } From fea3c40ede8e9e515b2eae39508273b7b8685249 Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Fri, 26 Jul 2024 13:52:37 -0400 Subject: [PATCH 38/82] Define build constants in CoreWidgetExtension (#3506) --- extensions/CoreWidgetProvider/CoreWidgetProvider.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj b/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj index 9f67c0df9d..a02733d09b 100644 --- a/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj +++ b/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj @@ -21,6 +21,11 @@ $(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml + + $(DefineConstants);CANARY_BUILD + $(DefineConstants);STABLE_BUILD + + From fe86d972e86a2719b23056c13b42def5efad2897 Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:18:46 -0400 Subject: [PATCH 39/82] Remove ProcessCounter from counters when no longer valid (#3511) --- extensions/CoreWidgetProvider/Helpers/CPUStats.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/extensions/CoreWidgetProvider/Helpers/CPUStats.cs b/extensions/CoreWidgetProvider/Helpers/CPUStats.cs index f701257e20..8b54a36e79 100644 --- a/extensions/CoreWidgetProvider/Helpers/CPUStats.cs +++ b/extensions/CoreWidgetProvider/Helpers/CPUStats.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Diagnostics; +using Serilog; namespace CoreWidgetProvider.Helpers; @@ -13,6 +14,8 @@ internal sealed class CPUStats : IDisposable private readonly PerformanceCounter _procFrequency = new("Processor Information", "Processor Frequency", "_Total"); private readonly Dictionary _cpuCounters = new(); + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(CPUStats)); + internal sealed class ProcessStats { public Process? Process { get; set; } @@ -70,8 +73,14 @@ public void GetData() // process might be terminated processCPUUsages.Add(processCounter.Key, processCounter.Value.NextValue() / Environment.ProcessorCount); } - catch + catch (InvalidOperationException) + { + _log.Information($"ProcessCounter Key {processCounter.Key} no longer exists, removing from _cpuCounters."); + _cpuCounters.Remove(processCounter.Key); + } + catch (Exception ex) { + _log.Error(ex, "Error going through process counters."); } } From 02b3bc38759798542316b1786cd8361923091385 Mon Sep 17 00:00:00 2001 From: Eric Johnson Date: Tue, 30 Jul 2024 13:54:15 -0700 Subject: [PATCH 40/82] Migrate to new nuget feed (#3515) --- nuget.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuget.config b/nuget.config index e28af753a5..84fb2018d0 100644 --- a/nuget.config +++ b/nuget.config @@ -7,7 +7,7 @@ - + From 51d09d800933a0bfa09032762bc4d4339c90eb4f Mon Sep 17 00:00:00 2001 From: Lauren Ciha <64796985+lauren-ciha@users.noreply.github.com> Date: Wed, 31 Jul 2024 09:36:39 -0700 Subject: [PATCH 41/82] Add information button name and TeachingTip narrations to Review and Finish page (#3497) * Add narration to generate config file info button * Make Generate Configuration info TeachingTip readable --- .../DevHome.SetupFlow/Strings/en-us/Resources.resw | 6 +++++- tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw index 938c7eaee2..35b62e934a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -661,7 +661,7 @@ Generate Configuration file Text for a generating configuration file button - + Generate a WinGet Configuration file (.winget) to repeat this setup in the future or share it with others. {Locked="WinGet",".winget"}Tooltip text about the generated configuration file @@ -2023,4 +2023,8 @@ Close Close button automation name for the login UI dialog + + Generate configuration file info + Narration for the informational icon next to the "Generate Configuration file" button + \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml index de8a04aa87..7554aaa937 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml @@ -79,6 +79,7 @@ x:Uid="Review_GenerateConfigurationFileButton"/> + + + + + + + diff --git a/tools/Customization/DevHome.Customization/Views/AddRepositoriesView.xaml.cs b/tools/Customization/DevHome.Customization/Views/AddRepositoriesView.xaml.cs new file mode 100644 index 0000000000..99ab31d0ab --- /dev/null +++ b/tools/Customization/DevHome.Customization/Views/AddRepositoriesView.xaml.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Extensions; +using DevHome.Common.Services; +using DevHome.Customization.Models; +using DevHome.Customization.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Customization.Views; + +public sealed partial class AddRepositoriesView : UserControl +{ + public FileExplorerViewModel ViewModel + { + get; + } + + public AddRepositoriesView() + { + ViewModel = Application.Current.GetService(); + this.InitializeComponent(); + } + + public void RemoveFolderButton_Click(object sender, RoutedEventArgs e) + { + // Extract relevant data from view and give to view model for remove + MenuFlyoutItem menuItem = (MenuFlyoutItem)sender; + if (menuItem.DataContext is RepositoryInformation repoInfo) + { + ViewModel.RemoveTrackedRepositoryFromDevHome(repoInfo.RepositoryRootPath); + } + } + + public void AssignSourceControlProviderButton_Click(object sender, RoutedEventArgs e) + { + // Extract relevant data from view and give to view model for assign + MenuFlyoutItem menuItem = (MenuFlyoutItem)sender; + if (menuItem.DataContext is RepositoryInformation repoInfo) + { + ViewModel.AssignSourceControlProviderToRepository(menuItem.Text, repoInfo.RepositoryRootPath); + } + } +} diff --git a/tools/Customization/DevHome.Customization/Views/FileExplorerPage.xaml b/tools/Customization/DevHome.Customization/Views/FileExplorerPage.xaml index bc8548405b..a4cc83f0af 100644 --- a/tools/Customization/DevHome.Customization/Views/FileExplorerPage.xaml +++ b/tools/Customization/DevHome.Customization/Views/FileExplorerPage.xaml @@ -4,13 +4,16 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:commonViews="using:DevHome.Common.Views" xmlns:behaviors="using:DevHome.Common.Behaviors" - xmlns:views="using:DevHome.Customization.Views" + xmlns:views="using:DevHome.Customization.Views" behaviors:NavigationViewHeaderBehavior.HeaderTemplate="{StaticResource BreadcrumbBarDataTemplate}" behaviors:NavigationViewHeaderBehavior.HeaderContext="{x:Bind ViewModel}"> - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/tools/Customization/DevHome.FileExplorerSourceControlIntegration/DevHome.FileExplorerSourceControlIntegration.csproj b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/DevHome.FileExplorerSourceControlIntegration.csproj new file mode 100644 index 0000000000..e5c5145efd --- /dev/null +++ b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/DevHome.FileExplorerSourceControlIntegration.csproj @@ -0,0 +1,39 @@ + + + + Exe + + + WinExe + + + + enable + false + x86;x64;arm64 + win-x86;win-x64;win-arm64 + enable + $(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Always + + + + + $(DefineConstants);DEBUG + + \ No newline at end of file diff --git a/tools/Customization/DevHome.FileExplorerSourceControlIntegration/NativeMethods.txt b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/NativeMethods.txt new file mode 100644 index 0000000000..5062808c47 --- /dev/null +++ b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/NativeMethods.txt @@ -0,0 +1,6 @@ +CoRegisterClassObject +CoRevokeClassObject +CoResumeClassObjects +MEMORYSTATUSEX +GlobalMemoryStatusEx +CoCreateInstance \ No newline at end of file diff --git a/tools/Customization/DevHome.FileExplorerSourceControlIntegration/Program.cs b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/Program.cs new file mode 100644 index 0000000000..8726f3ab66 --- /dev/null +++ b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/Program.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Configuration; +using Microsoft.Windows.AppLifecycle; +using Serilog; +using Windows.ApplicationModel.Activation; + +namespace FileExplorerSourceControlIntegration; + +public sealed class Program +{ + [MTAThread] + public static async Task Main([System.Runtime.InteropServices.WindowsRuntime.ReadOnlyArray] string[] args) + { + // Set up Logging + Environment.SetEnvironmentVariable("DEVHOME_LOGS_ROOT", Path.Join(DevHome.Common.Logging.LogFolderRoot, "FileExplorerSourceControlIntegration")); + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings_FileExplorerSourceControl.json") + .Build(); + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .CreateLogger(); + + Log.Information($"Launched with args: {string.Join(' ', args.ToArray())}"); + + // Force the app to be single instanced + // Get or register the main instance + var mainInstance = AppInstance.FindOrRegisterForKey("mainInstance"); + var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + + if (!mainInstance.IsCurrent) + { + Log.Information($"Not main instance, redirecting."); + await mainInstance.RedirectActivationToAsync(activationArgs); + Log.CloseAndFlush(); + return; + } + + if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer") + { + HandleCOMServerActivation(); + } + else + { + Log.Warning("Not being launched as a ComServer... exiting."); + } + + Log.CloseAndFlush(); + } + + private static void AppActivationRedirected(object sender, Microsoft.Windows.AppLifecycle.AppActivationArguments activationArgs) + { + Log.Information($"Redirected with kind: {activationArgs.Kind}"); + + // Handle COM server + if (activationArgs.Kind == ExtendedActivationKind.Launch) + { + var d = activationArgs.Data as ILaunchActivatedEventArgs; + var args = d?.Arguments.Split(); + + if (args?.Length > 1 && args[1] == "-RegisterProcessAsComServer") + { + Log.Information($"Activation COM Registration Redirect: {string.Join(' ', args.ToList())}"); + HandleCOMServerActivation(); + } + } + } + + private static void HandleCOMServerActivation() + { + Log.Information($"Activating COM Server"); + using var sourceControlProviderServer = new SourceControlProviderServer(); + var sourceControlProviderInstance = new SourceControlProvider(); + var wrapper = new Microsoft.Internal.Windows.DevHome.Helpers.FileExplorer.ExtraFolderPropertiesWrapper(sourceControlProviderInstance, sourceControlProviderInstance); + sourceControlProviderServer.RegisterSourceControlProviderServer(() => wrapper); + sourceControlProviderServer.Run(); + } +} diff --git a/tools/Customization/DevHome.FileExplorerSourceControlIntegration/Properties/launchSettings.json b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/Properties/launchSettings.json new file mode 100644 index 0000000000..ebaea7ef67 --- /dev/null +++ b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "FileExplorerSourceControlIntegration": { + "commandName": "Project", + "commandLineArgs": "-RegisterProcessAsComServer" + } + } +} \ No newline at end of file diff --git a/tools/Customization/DevHome.FileExplorerSourceControlIntegration/Services/RepoStoreOptions.cs b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/Services/RepoStoreOptions.cs new file mode 100644 index 0000000000..ddd4896a31 --- /dev/null +++ b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/Services/RepoStoreOptions.cs @@ -0,0 +1,27 @@ +// 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 DevHome.FileExplorerSourceControlIntegration.Services; + +public partial class RepoStoreOptions +{ + private const string RepoStoreFileNameDefault = "TrackedRepositoryStore.json"; + + public string RepoStoreFileName { get; set; } = RepoStoreFileNameDefault; + + private readonly string _repoStoreFolderPathDefault = Path.Combine(Path.GetTempPath(), "FileExplorerSourceControlIntegration"); + + private string? _repoStoreFolderPath; + + public string RepoStoreFolderPath + { + get => _repoStoreFolderPath is null ? _repoStoreFolderPathDefault : _repoStoreFolderPath; + set => _repoStoreFolderPath = string.IsNullOrEmpty(value) ? _repoStoreFolderPathDefault : value; + } +} diff --git a/tools/Customization/DevHome.FileExplorerSourceControlIntegration/Services/RepositoryTracking.cs b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/Services/RepositoryTracking.cs new file mode 100644 index 0000000000..1ff7807a08 --- /dev/null +++ b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/Services/RepositoryTracking.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Helpers; +using DevHome.Common.Services; +using DevHome.Common.TelemetryEvents.SourceControlIntegration; +using DevHome.Telemetry; +using Serilog; +using Windows.Storage; + +namespace DevHome.FileExplorerSourceControlIntegration.Services; + +public class RepositoryTracking +{ + public RepoStoreOptions RepoStoreOptions + { + get; set; + } + + public enum RepositoryChange + { + Added, + Removed, + } + + private readonly FileService _fileService; + + private readonly object _trackRepoLock = new(); + + private Dictionary TrackedRepositories { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + private readonly Serilog.ILogger _log = Log.ForContext("SourceContext", nameof(RepositoryTracking)); + + public event Windows.Foundation.TypedEventHandler? RepositoryChanged; + + public DateTime LastRestore { get; set; } + + public RepositoryTracking(string? path) + { + if (RuntimeHelper.IsMSIX) + { + RepoStoreOptions = new RepoStoreOptions + { + RepoStoreFolderPath = ApplicationData.Current.LocalFolder.Path, + }; + _log.Debug("Repo Store for File Explorer Integration created under ApplicationData"); + } + else + { + RepoStoreOptions = new RepoStoreOptions + { + RepoStoreFolderPath = path ?? string.Empty, + }; + } + + _fileService = new FileService(); + RestoreTrackedRepositoriesFomJson(); + } + + public void RestoreTrackedRepositoriesFomJson() + { + lock (_trackRepoLock) + { + var caseSensitiveDictionary = _fileService.Read>(RepoStoreOptions.RepoStoreFolderPath, RepoStoreOptions.RepoStoreFileName); + + // No repositories are currently being tracked. The file will be created on first add to repository tracking. + if (caseSensitiveDictionary == null) + { + TrackedRepositories = new Dictionary(StringComparer.OrdinalIgnoreCase); + _log.Debug("Repo store cache has just been created"); + } + else + { + TrackedRepositories = caseSensitiveDictionary.ToDictionary(entry => entry.Key, entry => entry.Value, StringComparer.OrdinalIgnoreCase); + } + + LastRestore = DateTime.Now; + } + + _log.Information($"Repositories retrieved from Repo Store, number of registered repositories: {TrackedRepositories.Count}"); + } + + public void AddRepositoryPath(string extensionCLSID, string rootPath) + { + lock (_trackRepoLock) + { + if (!TrackedRepositories.ContainsKey(rootPath)) + { + TrackedRepositories[rootPath] = extensionCLSID!; + _fileService.Save(RepoStoreOptions.RepoStoreFolderPath, RepoStoreOptions.RepoStoreFileName, TrackedRepositories); + _log.Information("Repository added to repo store"); + try + { + RepositoryChanged?.Invoke(extensionCLSID, RepositoryChange.Added); + } + catch (Exception ex) + { + _log.Error(ex, $"Added event signaling failed: "); + } + } + else + { + _log.Warning("Repository root path already registered in the repo store"); + } + } + + TelemetryFactory.Get().Log("AddEnhancedRepository_Event", LogLevel.Critical, new SourceControlIntegrationEvent(extensionCLSID, rootPath, TrackedRepositories.Count)); + } + + public void RemoveRepositoryPath(string rootPath) + { + var extensionCLSID = string.Empty; + lock (_trackRepoLock) + { + TrackedRepositories.TryGetValue(rootPath, out extensionCLSID); + TrackedRepositories.Remove(rootPath); + _fileService.Save(RepoStoreOptions.RepoStoreFolderPath, RepoStoreOptions.RepoStoreFileName, TrackedRepositories); + _log.Information("Repository removed from repo store"); + try + { + RepositoryChanged?.Invoke(extensionCLSID ??= string.Empty, RepositoryChange.Removed); + } + catch (Exception ex) + { + _log.Error(ex, $"Removed event signaling failed: "); + } + } + + TelemetryFactory.Get().Log("RemoveEnhancedRepository_Event", LogLevel.Critical, new SourceControlIntegrationEvent(extensionCLSID ?? string.Empty, rootPath, TrackedRepositories.Count)); + } + + public Dictionary GetAllTrackedRepositories() + { + lock (_trackRepoLock) + { + ReloadRepositoryStoreIfChangesDetected(); + _log.Information("All repositories retrieved from repo store"); + return TrackedRepositories; + } + } + + public string GetSourceControlProviderForRootPath(string rootPath) + { + lock (_trackRepoLock) + { + ReloadRepositoryStoreIfChangesDetected(); + if (TrackedRepositories.TryGetValue(rootPath, out var value)) + { + _log.Information("Source Control Provider returned for root path"); + return TrackedRepositories[rootPath]; + } + else + { + _log.Error("The root path is not registered for File Explorer Source Control Integration"); + return string.Empty; + } + } + } + + public void ModifySourceControlProviderForTrackedRepository(string extensionCLSID, string rootPath) + { + lock (_trackRepoLock) + { + if (TrackedRepositories.TryGetValue(rootPath, out var existingExtensionCLSID)) + { + TrackedRepositories[rootPath] = extensionCLSID; + _fileService.Save(RepoStoreOptions.RepoStoreFolderPath, RepoStoreOptions.RepoStoreFileName, TrackedRepositories); + _log.Information("Source control extension for tracked repository modified"); + } + else + { + _log.Error("The root path is not registered for File Explorer Source Control Integration"); + } + } + } + + public void ReloadRepositoryStoreIfChangesDetected() + { + var lastTimeModified = System.IO.File.GetLastWriteTime(Path.Combine(RepoStoreOptions.RepoStoreFolderPath, RepoStoreOptions.RepoStoreFileName)); + _log.Information("Last Time Modified: {0}", lastTimeModified); + if (DateTime.Compare(LastRestore, lastTimeModified) < 0) + { + RestoreTrackedRepositoriesFomJson(); + _log.Information("Tracked repositories restored from JSON at {0}", DateTime.Now); + } + } +} diff --git a/tools/Customization/DevHome.FileExplorerSourceControlIntegration/SourceControlProvider.cs b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/SourceControlProvider.cs new file mode 100644 index 0000000000..d6c527ce0a --- /dev/null +++ b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/SourceControlProvider.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.InteropServices; +using DevHome.FileExplorerSourceControlIntegration.Services; +using Microsoft.Internal.Windows.DevHome.Helpers.FileExplorer; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using Windows.Foundation.Collections; +using Windows.Win32; +using Windows.Win32.System.Com; +using WinRT; + +namespace FileExplorerSourceControlIntegration; + +#nullable enable +[ComVisible(true)] +#if CANARY_BUILD +[Guid("8DDE51FC-3AE8-4880-BD85-CA57DF7E2889")] +#elif STABLE_BUILD +[Guid("1212F95B-257E-414e-B44F-F26634BD2627")] +#else +[Guid("40FE4D6E-C9A0-48B4-A83E-AAA1D002C0D5")] +#endif +public class SourceControlProvider : + Microsoft.Internal.Windows.DevHome.Helpers.FileExplorer.IExtraFolderPropertiesHandler, + Microsoft.Internal.Windows.DevHome.Helpers.FileExplorer.IPerFolderRootSelector +{ + private readonly Serilog.ILogger _log = Log.ForContext("SourceContext", nameof(SourceControlProvider)); + private readonly RepositoryTracking _repositoryTracker; + + public SourceControlProvider() + { + _repositoryTracker = new RepositoryTracking(null); + } + + public Microsoft.Internal.Windows.DevHome.Helpers.FileExplorer.IPerFolderRootPropertyProvider? GetProvider(string rootPath) + { + ILocalRepositoryProvider localRepositoryProvider = GetLocalProvider(rootPath); + GetLocalRepositoryResult result = localRepositoryProvider.GetRepository(rootPath); + if (result.Result.Status == ProviderOperationStatus.Failure) + { + _log.Information("Could not open local repository."); + _log.Information(result.Result.DisplayMessage); + return null; + } + + return new RootFolderPropertyProvider(result.Repository); + } + + internal ILocalRepositoryProvider GetLocalProvider(string rootPath) + { + // TODO: Iterate extensions to find the correct one for this rootPath. + ILocalRepositoryProvider? provider = null; + var providerPtr = IntPtr.Zero; + try + { + var activationGUID = _repositoryTracker.GetSourceControlProviderForRootPath(rootPath); + + var hr = PInvoke.CoCreateInstance(Guid.Parse(activationGUID), null, CLSCTX.CLSCTX_LOCAL_SERVER, typeof(ILocalRepositoryProvider).GUID, out var extensionObj); + providerPtr = Marshal.GetIUnknownForObject(extensionObj); + if (hr < 0) + { + Log.Debug("Failure occurred while creating instance of repository provider"); + Marshal.ThrowExceptionForHR(hr); + } + + provider = MarshalInterface.FromAbi(providerPtr); + } + finally + { + if (providerPtr != IntPtr.Zero) + { + Marshal.Release(providerPtr); + } + } + + Log.Information("GetLocalProvider succeeded"); + return provider; + } + + IDictionary IExtraFolderPropertiesHandler.GetProperties(string[] propertyStrings, string rootFolderPath, string relativePath) + { + var localProvider = GetLocalProvider(rootFolderPath); + var localProviderResult = localProvider.GetRepository(rootFolderPath); + if (localProviderResult.Result.Status == ProviderOperationStatus.Failure) + { + _log.Warning("Could not open local repository."); + _log.Warning(localProviderResult.Result.DisplayMessage); + throw new ArgumentException(localProviderResult.Result.DisplayMessage); + } + + return localProviderResult.Repository.GetProperties(propertyStrings, relativePath); + } +} + +internal sealed class RootFolderPropertyProvider : Microsoft.Internal.Windows.DevHome.Helpers.FileExplorer.IPerFolderRootPropertyProvider +{ + public RootFolderPropertyProvider(ILocalRepository repository) + { + _repository = repository; + } + + public IPropertySet GetProperties(string[] properties, string relativePath) + { + return _repository.GetProperties(properties, relativePath); + } + + private readonly ILocalRepository _repository; +} diff --git a/tools/Customization/DevHome.FileExplorerSourceControlIntegration/SourceControlProviderFactory`1.cs b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/SourceControlProviderFactory`1.cs new file mode 100644 index 0000000000..fab09d53f3 --- /dev/null +++ b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/SourceControlProviderFactory`1.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.InteropServices; +using WinRT; + +namespace COM; + +[ComVisible(true)] +public class SourceControlProviderFactory : IClassFactory +{ + private readonly Func _createSourceControlProvider; + + public SourceControlProviderFactory(Func createSourceControlProvider) + { + _createSourceControlProvider = createSourceControlProvider; + } + + public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject) + { + ppvObject = IntPtr.Zero; + + if (pUnkOuter != IntPtr.Zero) + { + Marshal.ThrowExceptionForHR(CLASSENOAGGREGATION); + } + + // TODO: How to detect between WinRT/IInspectable and COM/IUnknown interfaces + ppvObject = MarshalInspectable.CreateMarshaler2(_createSourceControlProvider(), riid, true).Detach(); + + return 0; + } + + int IClassFactory.LockServer(bool fLock) + { + return 0; + } + + private const int CLASSENOAGGREGATION = unchecked((int)0x80040110); + private const int ENOINTERFACE = unchecked((int)0x80004002); +} + +internal static class Guids +{ + public const string IClassFactory = "00000001-0000-0000-C000-000000000046"; + public const string IUnknown = "00000000-0000-0000-C000-000000000046"; +} + +// IClassFactory declaration +[ComImport] +[ComVisible(false)] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +[Guid(COM.Guids.IClassFactory)] +internal interface IClassFactory +{ + [PreserveSig] + int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject); + + [PreserveSig] + int LockServer(bool fLock); +} diff --git a/tools/Customization/DevHome.FileExplorerSourceControlIntegration/SourceControlProviderServer.cs b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/SourceControlProviderServer.cs new file mode 100644 index 0000000000..c4809e2719 --- /dev/null +++ b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/SourceControlProviderServer.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using COM; +using Serilog; +using Windows.Win32; +using Windows.Win32.System.Com; + +namespace FileExplorerSourceControlIntegration; + +public sealed class SourceControlProviderServer : IDisposable +{ + private readonly HashSet _registrationCookies = new(); + private readonly Serilog.ILogger _log = Log.ForContext("SourceContext", nameof(SourceControlProviderServer)); + + [UnconditionalSuppressMessage( + "ReflectionAnalysis", + "IL2050:COMCorrectness", + Justification = "SourceControlProviderFactory and all the interfaces it implements are defined in an assembly that is not marked trimmable which means the relevant interfaces won't be trimmed.")] + public void RegisterSourceControlProviderServer(Func createSourceControlProviderServer) + { + var clsid = typeof(SourceControlProvider).GUID; + + _log.Debug($"Registering class object:"); + _log.Debug($"CLSID: {clsid:B}"); + _log.Debug($"Type: {typeof(T)}"); + + uint cookie; + var hr = PInvoke.CoRegisterClassObject( + clsid, + new SourceControlProviderFactory(createSourceControlProviderServer), + CLSCTX.CLSCTX_LOCAL_SERVER, + Ole32.REGCLS_MULTIPLEUSE | Ole32.REGCLS_SUSPENDED, + out cookie); + + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + + _registrationCookies.Add(cookie); + _log.Debug($"Cookie: {cookie}"); + hr = PInvoke.CoResumeClassObjects(); + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + } + + public void Run() + { + // TODO : We need to handle lifetime management of the server. + // For details around ref counting and locking of out-of-proc COM servers, see + // https://docs.microsoft.com/windows/win32/com/out-of-process-server-implementation-helpers + // https://github.com/microsoft/devhome/issues/645 + Console.ReadLine(); + var disposedEvent = new ManualResetEvent(false); + disposedEvent.WaitOne(); + } + + public void Dispose() + { + _log.Debug($"Revoking class object registrations:"); + foreach (var cookie in _registrationCookies) + { + _log.Debug($"Cookie: {cookie}"); + var hr = PInvoke.CoRevokeClassObject(cookie); + Debug.Assert(hr >= 0, $"CoRevokeClassObject failed ({hr:x}). Cookie: {cookie}"); + } + } + + private sealed class Ole32 + { +#pragma warning disable SA1310 // Field names should not contain underscore + // https://docs.microsoft.com/windows/win32/api/combaseapi/ne-combaseapi-regcls + public const REGCLS REGCLS_MULTIPLEUSE = (REGCLS)1; + public const REGCLS REGCLS_SUSPENDED = (REGCLS)4; +#pragma warning restore SA1310 // Field names should not contain underscore + } +} diff --git a/tools/Customization/DevHome.FileExplorerSourceControlIntegration/appsettings_FileExplorerSourceControl.json b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/appsettings_FileExplorerSourceControl.json new file mode 100644 index 0000000000..ae429ca909 --- /dev/null +++ b/tools/Customization/DevHome.FileExplorerSourceControlIntegration/appsettings_FileExplorerSourceControl.json @@ -0,0 +1,31 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Debug" ], + "MinimumLevel": "Debug", + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "restrictedToMinimumLevel": "Debug" + } + }, + { + "Name": "File", + "Args": { + "path": "%DEVHOME_LOGS_ROOT%\\FileExplorerSourceControlIntegration.dhlog", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "restrictedToMinimumLevel": "Information", + "rollingInterval": "Day" + } + }, + { + "Name": "Debug" + } + ], + "Enrich": [ "FromLogContext" ], + "Properties": { + "SourceContext": "FileExplorerSourceControlIntegration" + } + } +} \ No newline at end of file diff --git a/tools/Customization/DevHome.FileExplorerSourceControlIntegrationUnitTest/DevHome.FileExplorerSourceControlIntegrationUnitTest.csproj b/tools/Customization/DevHome.FileExplorerSourceControlIntegrationUnitTest/DevHome.FileExplorerSourceControlIntegrationUnitTest.csproj new file mode 100644 index 0000000000..9bbea8d3eb --- /dev/null +++ b/tools/Customization/DevHome.FileExplorerSourceControlIntegrationUnitTest/DevHome.FileExplorerSourceControlIntegrationUnitTest.csproj @@ -0,0 +1,24 @@ + + + + DevHome.FileExplorerSourceControlIntegrationUnitTest + x86;x64;arm64 + win-x86;win-x64;win-arm64 + false + enable + enable + true + true + resources.pri + + + + + + + + + + + + diff --git a/tools/Customization/DevHome.FileExplorerSourceControlIntegrationUnitTest/RepositoryTrackingServiceUnitTest.cs b/tools/Customization/DevHome.FileExplorerSourceControlIntegrationUnitTest/RepositoryTrackingServiceUnitTest.cs new file mode 100644 index 0000000000..993cb5897b --- /dev/null +++ b/tools/Customization/DevHome.FileExplorerSourceControlIntegrationUnitTest/RepositoryTrackingServiceUnitTest.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using DevHome.FileExplorerSourceControlIntegration.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DevHome.FileExplorerSourceControlIntegrationUnitTest; + +[TestClass] +public class RepositoryTrackingServiceUnitTest +{ + private RepositoryTracking RepoTracker { get; set; } = new(Path.Combine(Path.GetTempPath())); + + private readonly string _extension = "testExtension"; + + private readonly string _rootPath = "c:\\test\\rootPath"; + + private readonly string _caseAlteredRootPath = "C:\\TEST\\ROOTPATH"; + + [TestMethod] + public void AddRepository() + { + RepoTracker.AddRepositoryPath(_extension, _rootPath); + var result = RepoTracker.GetAllTrackedRepositories(); + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Count); + Assert.IsTrue(result.ContainsKey(_rootPath)); + Assert.IsTrue(result.ContainsValue(_extension)); + RepoTracker.RemoveRepositoryPath(_rootPath); + } + + [TestMethod] + public void RemoveRepository() + { + RepoTracker.AddRepositoryPath(_extension, _rootPath); + var result = RepoTracker.GetAllTrackedRepositories(); + Assert.AreEqual(1, result.Count); + RepoTracker.RemoveRepositoryPath(_rootPath); + result = RepoTracker.GetAllTrackedRepositories(); + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void GetAllRepositories() + { + for (var i = 0; i < 5; i++) + { + RepoTracker.AddRepositoryPath(_extension, string.Concat(_rootPath, i)); + } + + var result = RepoTracker.GetAllTrackedRepositories(); + Assert.IsNotNull(result); + Assert.AreEqual(5, result.Count); + Assert.IsTrue(result.ContainsKey(string.Concat(_rootPath, 0))); + Assert.IsTrue(result.ContainsValue(_extension)); + + for (var i = 0; i < 5; i++) + { + RepoTracker.RemoveRepositoryPath(string.Concat(_rootPath, i)); + } + } + + [TestMethod] + public void GetAllRepositories_Empty() + { + var result = RepoTracker.GetAllTrackedRepositories(); + Assert.IsNotNull(result); + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void GetSourceControlProviderFromRepositoryPath() + { + RepoTracker.AddRepositoryPath(_extension, _rootPath); + var result = RepoTracker.GetSourceControlProviderForRootPath(_rootPath); + Assert.IsNotNull(result); + Assert.AreEqual(_extension, result); + RepoTracker.RemoveRepositoryPath(_rootPath); + } + + [TestMethod] + public void AddRepository_DoesNotAddDuplicateValues() + { + RepoTracker.AddRepositoryPath(_extension, _rootPath); + + // Atempt to add duplicate entry that is altered in case + RepoTracker.AddRepositoryPath(_extension, _caseAlteredRootPath); + + // Ensure duplicate is not added and count is 1 + var result = RepoTracker.GetAllTrackedRepositories(); + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Count); + Assert.IsTrue(result.ContainsKey(_rootPath)); + Assert.IsTrue(result.ContainsValue(_extension)); + + RepoTracker.RemoveRepositoryPath(_rootPath); + } + + [TestCleanup] + public void TestCleanup() + { + if (File.Exists(Path.Combine(Path.GetTempPath(), "TrackedRepositoryStore.json"))) + { + File.Delete(Path.Combine(Path.GetTempPath(), "TrackedRepositoryStore.json")); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj index ead4c01c72..b517371d5f 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj @@ -9,7 +9,6 @@ - all From d8380dada4001eb215c878d9cdc67840dafe25ee Mon Sep 17 00:00:00 2001 From: Tim Kurtzman <49733346+timkur@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:36:18 -0700 Subject: [PATCH 59/82] Project Ironsides - Debugger Analyzer tools feature (#3550) * Debugger Analyzer tools feature * PR updates * Updated PR feedback * More PR feedback --- .../DevHome.PI/Controls/AddToolControl.xaml | 31 +- .../Controls/AddToolControl.xaml.cs | 15 +- .../DevHome.PI/Controls/EditToolsControl.xaml | 2 +- .../Controls/EditToolsControl.xaml.cs | 6 +- .../Controls/ExternalToolsManagementButton.cs | 9 +- .../Helpers/ClipboardMonitorInternalTool.cs | 13 +- tools/PI/DevHome.PI/Helpers/CommonHelper.cs | 190 +++++----- tools/PI/DevHome.PI/Helpers/ExternalTool.cs | 181 ++++++---- .../DevHome.PI/Helpers/ExternalToolsHelper.cs | 10 +- .../DevHome.PI/Helpers/InternalToolsHelper.cs | 4 +- tools/PI/DevHome.PI/Helpers/Tool.cs | 46 ++- .../PI/DevHome.PI/Models/ClipboardMonitor.cs | 4 +- tools/PI/DevHome.PI/Models/WERAnalysis.cs | 114 ++++++ .../PI/DevHome.PI/Models/WERAnalysisReport.cs | 79 ++++ tools/PI/DevHome.PI/Models/WERAnalyzer.cs | 184 ++++++++++ tools/PI/DevHome.PI/Models/WERDisplayInfo.cs | 42 --- tools/PI/DevHome.PI/Models/WERHelper.cs | 18 + tools/PI/DevHome.PI/PIApp.xaml.cs | 5 + tools/PI/DevHome.PI/Pages/WERPage.xaml | 16 +- tools/PI/DevHome.PI/Pages/WERPage.xaml.cs | 85 ++++- .../DevHome.PI/Strings/en-us/Resources.resw | 41 ++- .../ViewModels/BarWindowViewModel.cs | 8 +- .../DevHome.PI/ViewModels/WERPageViewModel.cs | 87 +++-- .../Views/BarWindowHorizontal.xaml.cs | 14 +- .../DevHome.PI/Views/BarWindowVertical.xaml | 4 +- .../Views/BarWindowVertical.xaml.cs | 336 +++++++++--------- .../PI/DevHome.PI/Views/PrimaryWindow.xaml.cs | 1 - .../PI/TestTools/TestDumpAnalyzer/Program.cs | 35 ++ .../TestDumpAnalyzer/TestDumpAnalyzer.csproj | 10 + .../TestDumpAnalyzer/TestDumpAnalyzer.sln | 37 ++ 30 files changed, 1165 insertions(+), 462 deletions(-) create mode 100644 tools/PI/DevHome.PI/Models/WERAnalysis.cs create mode 100644 tools/PI/DevHome.PI/Models/WERAnalysisReport.cs create mode 100644 tools/PI/DevHome.PI/Models/WERAnalyzer.cs delete mode 100644 tools/PI/DevHome.PI/Models/WERDisplayInfo.cs create mode 100644 tools/PI/TestTools/TestDumpAnalyzer/Program.cs create mode 100644 tools/PI/TestTools/TestDumpAnalyzer/TestDumpAnalyzer.csproj create mode 100644 tools/PI/TestTools/TestDumpAnalyzer/TestDumpAnalyzer.sln diff --git a/tools/PI/DevHome.PI/Controls/AddToolControl.xaml b/tools/PI/DevHome.PI/Controls/AddToolControl.xaml index 389912ee71..a65e974fca 100644 --- a/tools/PI/DevHome.PI/Controls/AddToolControl.xaml +++ b/tools/PI/DevHome.PI/Controls/AddToolControl.xaml @@ -18,6 +18,8 @@ + + @@ -123,11 +125,30 @@ - - - + + + + + + + + + + + + + + + + diff --git a/tools/PI/DevHome.PI/Pages/ModulesPage.xaml b/tools/PI/DevHome.PI/Pages/ModulesPage.xaml index 99226989cc..7d69802a2d 100644 --- a/tools/PI/DevHome.PI/Pages/ModulesPage.xaml +++ b/tools/PI/DevHome.PI/Pages/ModulesPage.xaml @@ -23,7 +23,12 @@ - diff --git a/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml b/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml index dbc6615020..2ba507162d 100644 --- a/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml +++ b/tools/PI/DevHome.PI/Pages/WinLogsPage.xaml @@ -20,7 +20,12 @@ - Is running as System This refers to whether the application is running as System - - Restart Project Ironsides as Admin to get more data - {Locked="Ironsides"} When the user clicks this button we close and relaunch Project Ironsides as admin to get more app details - Go to the key insights The tooltip for a button that takes the user to the key insights page @@ -1069,6 +1065,14 @@ Project Ironsides needs to run as Admin in order to modify WER collection for an app. {Locked="Ironsides"} Tooltip for a button that relaunches Project Ironsides as admin to change WER settings + + Get Information + The contents of a button to allow the user to trigger elevation to get more information + + + Project Ironsides needs to run as Admin in order to get more data for an app. + {Locked="Ironsides"} Tooltip for a button that relaunches Project Ironsides as admin to get more data + Dump Info Selector Pivot that shows information about a Dump File From c126ed2bb97e16b89d4c60c99dfe9318f5a95471 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Wed, 7 Aug 2024 14:05:33 -0700 Subject: [PATCH 66/82] Auto focus on page load (#3325) --- common/Behaviors/AutoFocusBehavior.cs | 21 ++++++++++++++++ common/Views/DevHomePage.cs | 25 ------------------- common/Views/DevHomeUserControl.cs | 25 ------------------- common/Views/ToolPage.cs | 14 ++++++++++- .../DevHome.Settings/Views/AboutPage.xaml | 4 +-- .../DevHome.Settings/Views/AboutPage.xaml.cs | 2 +- .../DevHome.Settings/Views/AccountsPage.xaml | 5 ++-- .../Views/AccountsPage.xaml.cs | 2 +- .../Views/ExperimentalFeaturesPage.xaml | 4 +-- .../Views/ExperimentalFeaturesPage.xaml.cs | 2 +- .../DevHome.Settings/Views/FeedbackPage.xaml | 4 +-- .../Views/FeedbackPage.xaml.cs | 2 +- .../Views/PreferencesPage.xaml | 4 +-- .../Views/PreferencesPage.xaml.cs | 2 +- .../DevHome.Settings/Views/SettingsPage.xaml | 4 +-- .../Views/SettingsPage.xaml.cs | 2 +- src/Views/WhatsNewPage.xaml | 6 ++--- src/Views/WhatsNewPage.xaml.cs | 2 +- .../Views/DevDriveInsightsPage.xaml | 4 +-- .../Views/DevDriveInsightsPage.xaml.cs | 2 +- .../Views/DevDriveInsightsView.xaml | 4 +-- .../Views/DevDriveInsightsView.xaml.cs | 4 +-- .../Views/FileExplorerPage.xaml | 4 +-- .../Views/FileExplorerPage.xaml.cs | 2 +- .../Views/GeneralSystemPage.xaml | 4 +-- .../Views/GeneralSystemPage.xaml.cs | 2 +- .../Views/ExtensionSettingsPage.xaml | 4 +-- .../Views/ExtensionSettingsPage.xaml.cs | 2 +- .../PI/DevHome.PI/Pages/PreferencesPage.xaml | 3 +++ .../Views/AppManagementView.xaml | 2 ++ .../EnvironmentCreationOptionsView.xaml | 6 ++++- 31 files changed, 82 insertions(+), 91 deletions(-) create mode 100644 common/Behaviors/AutoFocusBehavior.cs delete mode 100644 common/Views/DevHomePage.cs delete mode 100644 common/Views/DevHomeUserControl.cs diff --git a/common/Behaviors/AutoFocusBehavior.cs b/common/Behaviors/AutoFocusBehavior.cs new file mode 100644 index 0000000000..9f99cd7adc --- /dev/null +++ b/common/Behaviors/AutoFocusBehavior.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.WinUI.Behaviors; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Common.Behaviors; + +/// +/// This behavior automatically sets the focus on the associated when it is loaded. +/// +/// +/// This implementation is based on the code from the Windows Community Toolkit. +/// Reference: https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/winui/CommunityToolkit.WinUI.UI.Behaviors/Focus/AutoFocusBehavior.cs +/// Issue: https://github.com/CommunityToolkit/Windows/issues/443 +/// +public sealed class AutoFocusBehavior : BehaviorBase +{ + protected override void OnAssociatedObjectLoaded() => AssociatedObject.Focus(FocusState.Programmatic); +} diff --git a/common/Views/DevHomePage.cs b/common/Views/DevHomePage.cs deleted file mode 100644 index dfcf7d79f2..0000000000 --- a/common/Views/DevHomePage.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; - -namespace DevHome.Common.Views; - -/// -/// This page is used to auto focus on the first selectable element. -/// Please inherit from this class for pages. -/// If the Page needs custom focus logic (for example, waiting until adaptive cards are loaded) -/// the individual Page should handle that. Take a look at EnvironmentCreationOptionsView.xaml -/// for an example on using the autofocus behavior to focus when the element when it becomes visible. -/// -public class DevHomePage : Page -{ - public DevHomePage() - { - Loaded += (s, e) => - { - Focus(FocusState.Programmatic); - }; - } -} diff --git a/common/Views/DevHomeUserControl.cs b/common/Views/DevHomeUserControl.cs deleted file mode 100644 index 0d8f55344a..0000000000 --- a/common/Views/DevHomeUserControl.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; - -namespace DevHome.Common.Views; - -/// -/// This UserControl is used to auto focus on the first selectable element. -/// Please inherit from this class for UserControl. -/// If the UserControl needs custom focus logic (for example, waiting until adaptive cards are loaded) -/// the individual UserControl should handle that. Take a look at EnvironmentCreationOptionsView.xaml -/// for an example on using the autofocus behavior to focus when the element when it becomes visible. -/// -public class DevHomeUserControl : UserControl -{ - public DevHomeUserControl() - { - Loaded += (s, args) => - { - Focus(FocusState.Programmatic); - }; - } -} diff --git a/common/Views/ToolPage.cs b/common/Views/ToolPage.cs index a34529858f..7f227bd2e9 100644 --- a/common/Views/ToolPage.cs +++ b/common/Views/ToolPage.cs @@ -1,8 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + namespace DevHome.Common.Views; -public abstract class ToolPage : DevHomePage +public abstract class ToolPage : Page { + public ToolPage() + { + Loaded += OnLoaded; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + Focus(FocusState.Programmatic); + } } diff --git a/settings/DevHome.Settings/Views/AboutPage.xaml b/settings/DevHome.Settings/Views/AboutPage.xaml index 6ac173b743..f3d9367aaf 100644 --- a/settings/DevHome.Settings/Views/AboutPage.xaml +++ b/settings/DevHome.Settings/Views/AboutPage.xaml @@ -1,4 +1,4 @@ - - + diff --git a/settings/DevHome.Settings/Views/AboutPage.xaml.cs b/settings/DevHome.Settings/Views/AboutPage.xaml.cs index 79b8c342c0..74899c312a 100644 --- a/settings/DevHome.Settings/Views/AboutPage.xaml.cs +++ b/settings/DevHome.Settings/Views/AboutPage.xaml.cs @@ -12,7 +12,7 @@ namespace DevHome.Settings.Views; -public sealed partial class AboutPage : DevHomePage +public sealed partial class AboutPage : ToolPage { public AboutViewModel ViewModel { get; } diff --git a/settings/DevHome.Settings/Views/AccountsPage.xaml b/settings/DevHome.Settings/Views/AccountsPage.xaml index 7682d530ab..a9bf9cea0b 100644 --- a/settings/DevHome.Settings/Views/AccountsPage.xaml +++ b/settings/DevHome.Settings/Views/AccountsPage.xaml @@ -1,4 +1,4 @@ - - @@ -73,4 +72,4 @@ - + diff --git a/settings/DevHome.Settings/Views/AccountsPage.xaml.cs b/settings/DevHome.Settings/Views/AccountsPage.xaml.cs index f49fec7556..63d7c00853 100644 --- a/settings/DevHome.Settings/Views/AccountsPage.xaml.cs +++ b/settings/DevHome.Settings/Views/AccountsPage.xaml.cs @@ -19,7 +19,7 @@ namespace DevHome.Settings.Views; -public sealed partial class AccountsPage : DevHomePage +public sealed partial class AccountsPage : ToolPage { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(AccountsPage)); diff --git a/settings/DevHome.Settings/Views/ExperimentalFeaturesPage.xaml b/settings/DevHome.Settings/Views/ExperimentalFeaturesPage.xaml index e20164d602..e976c7165e 100644 --- a/settings/DevHome.Settings/Views/ExperimentalFeaturesPage.xaml +++ b/settings/DevHome.Settings/Views/ExperimentalFeaturesPage.xaml @@ -1,5 +1,5 @@ - - + diff --git a/settings/DevHome.Settings/Views/ExperimentalFeaturesPage.xaml.cs b/settings/DevHome.Settings/Views/ExperimentalFeaturesPage.xaml.cs index 61a48e0ddb..59306d9a25 100644 --- a/settings/DevHome.Settings/Views/ExperimentalFeaturesPage.xaml.cs +++ b/settings/DevHome.Settings/Views/ExperimentalFeaturesPage.xaml.cs @@ -8,7 +8,7 @@ namespace DevHome.Settings.Views; -public sealed partial class ExperimentalFeaturesPage : DevHomePage +public sealed partial class ExperimentalFeaturesPage : ToolPage { public ExperimentalFeaturesViewModel ViewModel { get; } diff --git a/settings/DevHome.Settings/Views/FeedbackPage.xaml b/settings/DevHome.Settings/Views/FeedbackPage.xaml index df06df3799..cc6ee00bb5 100644 --- a/settings/DevHome.Settings/Views/FeedbackPage.xaml +++ b/settings/DevHome.Settings/Views/FeedbackPage.xaml @@ -1,7 +1,7 @@ - - + diff --git a/settings/DevHome.Settings/Views/FeedbackPage.xaml.cs b/settings/DevHome.Settings/Views/FeedbackPage.xaml.cs index 2d736f266a..63247b0f83 100644 --- a/settings/DevHome.Settings/Views/FeedbackPage.xaml.cs +++ b/settings/DevHome.Settings/Views/FeedbackPage.xaml.cs @@ -26,7 +26,7 @@ namespace DevHome.Settings.Views; /// /// An empty page that can be used on its own or navigated to within a Frame. /// -public sealed partial class FeedbackPage : DevHomePage +public sealed partial class FeedbackPage : ToolPage { private static readonly double ByteSizeGB = 1024 * 1024 * 1024; private static string wmiCPUInfo = string.Empty; diff --git a/settings/DevHome.Settings/Views/PreferencesPage.xaml b/settings/DevHome.Settings/Views/PreferencesPage.xaml index 56160abed5..774f313243 100644 --- a/settings/DevHome.Settings/Views/PreferencesPage.xaml +++ b/settings/DevHome.Settings/Views/PreferencesPage.xaml @@ -1,4 +1,4 @@ - - + diff --git a/settings/DevHome.Settings/Views/PreferencesPage.xaml.cs b/settings/DevHome.Settings/Views/PreferencesPage.xaml.cs index 011eaaa75c..02ae6d37b6 100644 --- a/settings/DevHome.Settings/Views/PreferencesPage.xaml.cs +++ b/settings/DevHome.Settings/Views/PreferencesPage.xaml.cs @@ -9,7 +9,7 @@ namespace DevHome.Settings.Views; -public sealed partial class PreferencesPage : DevHomePage +public sealed partial class PreferencesPage : ToolPage { public PreferencesViewModel ViewModel { get; } diff --git a/settings/DevHome.Settings/Views/SettingsPage.xaml b/settings/DevHome.Settings/Views/SettingsPage.xaml index ed87c22c9b..5ec6b7936e 100644 --- a/settings/DevHome.Settings/Views/SettingsPage.xaml +++ b/settings/DevHome.Settings/Views/SettingsPage.xaml @@ -1,7 +1,7 @@ - - + diff --git a/settings/DevHome.Settings/Views/SettingsPage.xaml.cs b/settings/DevHome.Settings/Views/SettingsPage.xaml.cs index 7905389353..94a447af36 100644 --- a/settings/DevHome.Settings/Views/SettingsPage.xaml.cs +++ b/settings/DevHome.Settings/Views/SettingsPage.xaml.cs @@ -6,7 +6,7 @@ namespace DevHome.Settings.Views; -public sealed partial class SettingsPage : DevHomePage +public sealed partial class SettingsPage : ToolPage { public SettingsViewModel ViewModel { get; } diff --git a/src/Views/WhatsNewPage.xaml b/src/Views/WhatsNewPage.xaml index ca3b4b4767..069f853d16 100644 --- a/src/Views/WhatsNewPage.xaml +++ b/src/Views/WhatsNewPage.xaml @@ -1,4 +1,4 @@ - - + diff --git a/src/Views/WhatsNewPage.xaml.cs b/src/Views/WhatsNewPage.xaml.cs index e656bf3190..7b66dab6d6 100644 --- a/src/Views/WhatsNewPage.xaml.cs +++ b/src/Views/WhatsNewPage.xaml.cs @@ -18,7 +18,7 @@ namespace DevHome.Views; -public sealed partial class WhatsNewPage : DevHomePage +public sealed partial class WhatsNewPage : ToolPage { private readonly Uri _devDrivePageKeyUri = new("ms-settings:disksandvolumes"); private readonly Uri _devDriveLearnMoreLinkUri = new("https://go.microsoft.com/fwlink/?linkid=2236041"); diff --git a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml index 3bbafdf923..4db24127cb 100644 --- a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml +++ b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml @@ -1,4 +1,4 @@ - - + diff --git a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml.cs b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml.cs index ccb6f6b88f..133a883349 100644 --- a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml.cs +++ b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml.cs @@ -11,7 +11,7 @@ namespace DevHome.Customization.Views; -public sealed partial class DevDriveInsightsPage : DevHomePage +public sealed partial class DevDriveInsightsPage : ToolPage { public DevDriveInsightsViewModel ViewModel { diff --git a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsView.xaml b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsView.xaml index dd5adcaa07..e2af1a6657 100644 --- a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsView.xaml +++ b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsView.xaml @@ -1,4 +1,4 @@ - - + diff --git a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsView.xaml.cs b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsView.xaml.cs index 9f5718da84..40d8c19fe7 100644 --- a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsView.xaml.cs +++ b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsView.xaml.cs @@ -2,13 +2,13 @@ // Licensed under the MIT License. using DevHome.Common.Extensions; -using DevHome.Common.Views; using DevHome.Customization.ViewModels; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; namespace DevHome.Customization.Views; -public sealed partial class DevDriveInsightsView : DevHomeUserControl +public sealed partial class DevDriveInsightsView : UserControl { public DevDriveInsightsViewModel ViewModel { diff --git a/tools/Customization/DevHome.Customization/Views/FileExplorerPage.xaml b/tools/Customization/DevHome.Customization/Views/FileExplorerPage.xaml index a4cc83f0af..dba4e23323 100644 --- a/tools/Customization/DevHome.Customization/Views/FileExplorerPage.xaml +++ b/tools/Customization/DevHome.Customization/Views/FileExplorerPage.xaml @@ -1,4 +1,4 @@ - - \ No newline at end of file + diff --git a/tools/Customization/DevHome.Customization/Views/FileExplorerPage.xaml.cs b/tools/Customization/DevHome.Customization/Views/FileExplorerPage.xaml.cs index c08fa425f0..c21700fe33 100644 --- a/tools/Customization/DevHome.Customization/Views/FileExplorerPage.xaml.cs +++ b/tools/Customization/DevHome.Customization/Views/FileExplorerPage.xaml.cs @@ -8,7 +8,7 @@ namespace DevHome.Customization.Views; -public sealed partial class FileExplorerPage : DevHomePage +public sealed partial class FileExplorerPage : ToolPage { public FileExplorerViewModel ViewModel { diff --git a/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml b/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml index 36b635db89..9c394e5dd3 100644 --- a/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml +++ b/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml @@ -1,4 +1,4 @@ - - + diff --git a/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml.cs b/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml.cs index ac3e7e71ad..1ff40b7881 100644 --- a/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml.cs +++ b/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml.cs @@ -8,7 +8,7 @@ namespace DevHome.Customization.Views; -public sealed partial class GeneralSystemPage : DevHomePage +public sealed partial class GeneralSystemPage : ToolPage { public GeneralSystemViewModel ViewModel { diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionSettingsPage.xaml b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionSettingsPage.xaml index cdffafd7a7..94760dce5d 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionSettingsPage.xaml +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionSettingsPage.xaml @@ -1,7 +1,7 @@ - - + diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionSettingsPage.xaml.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionSettingsPage.xaml.cs index d165703230..2d9a76e5c6 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionSettingsPage.xaml.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionSettingsPage.xaml.cs @@ -8,7 +8,7 @@ namespace DevHome.ExtensionLibrary.Views; -public sealed partial class ExtensionSettingsPage : DevHomePage +public sealed partial class ExtensionSettingsPage : ToolPage { public ExtensionSettingsViewModel ViewModel { get; } diff --git a/tools/PI/DevHome.PI/Pages/PreferencesPage.xaml b/tools/PI/DevHome.PI/Pages/PreferencesPage.xaml index 4f6b020c6b..2cf1bcd2e7 100644 --- a/tools/PI/DevHome.PI/Pages/PreferencesPage.xaml +++ b/tools/PI/DevHome.PI/Pages/PreferencesPage.xaml @@ -9,6 +9,9 @@ xmlns:xaml="using:Microsoft.UI.Xaml" xmlns:behaviors="using:DevHome.Common.Behaviors" Loaded="Page_Loaded"> + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml index e5e14aeb1e..a187dd6aa3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml @@ -17,6 +17,7 @@ xmlns:ic="using:Microsoft.Xaml.Interactions.Core" xmlns:i="using:Microsoft.Xaml.Interactivity" xmlns:common="using:DevHome.Common.Controls" + xmlns:commonBehaviors="using:DevHome.Common.Behaviors" mc:Ignorable="d"> @@ -29,6 +30,7 @@ + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml index 6235d9285d..b37d9d34b5 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml @@ -6,7 +6,8 @@ xmlns:setupControls="using:DevHome.SetupFlow.Controls" xmlns:converters="using:CommunityToolkit.WinUI.Converters" xmlns:i="using:Microsoft.Xaml.Interactivity" - xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors" + xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors" + xmlns:commonBehaviors="using:DevHome.Common.Behaviors" Unloaded="ViewUnloaded" Loaded="ViewLoaded"> @@ -14,6 +15,9 @@ + + + From 7469978674fab1389fb5fe966f599c2b17b1091e Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Thu, 8 Aug 2024 10:12:14 -0700 Subject: [PATCH 67/82] Wrap Commit result and fetch from the command line (#3562) * Wrapp Commit result and fetch from the command line * Finish build fix with CommitWrapper * Properly split and utf-8 encode the git output * Configurability for whether to use the command line, and fall back if command line isn't available. * Use an LRU cache on commit log * Trim down unused methods from LruCacheDictionary * PR feedback --- .../Models/CommitLogCache.cs | 145 ++++++++++++++++-- .../Models/CommitWrapper.cs | 35 +++++ .../Models/GitExecute.cs | 1 + .../Models/GitLocalRepository.cs | 124 +++------------ .../Models/LruCacheDictionary.cs | 90 +++++++++++ .../Models/RepositoryWrapper.cs | 8 +- 6 files changed, 284 insertions(+), 119 deletions(-) create mode 100644 extensions/GitExtension/FileExplorerGitIntegration/Models/CommitWrapper.cs create mode 100644 extensions/GitExtension/FileExplorerGitIntegration/Models/LruCacheDictionary.cs diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitLogCache.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitLogCache.cs index 1b10c3c2c1..6820d0ea3b 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitLogCache.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitLogCache.cs @@ -17,32 +17,145 @@ namespace FileExplorerGitIntegration.Models; // Furthermore, LibGit2 revwalk initialization takes locks on internal data, which causes contention in multithreaded scenarios as threads // all scramble to initialize and re-initialize their own revwalk objects. // Ideally, LibGit2Sharp improves the API to allow reusing the revwalk object, but that seems unlikely to happen soon. -internal sealed class CommitLogCache : IEnumerable +internal sealed class CommitLogCache { private readonly List _commits = new(); + private readonly string _workingDirectory; + + // For now, we'll use the command line to get the last commit for a file, on demand. + // In the future we may use some sort of heuristic to determine if we should use the command line or not. + private readonly bool _preferCommandLine = true; + private readonly bool _useCommandLine; + private readonly GitDetect _gitDetect = new(); + private readonly bool _gitInstalled; + + private readonly LruCacheDictionary _cache = new(); public CommitLogCache(Repository repo) { - // For now, greedily get the entire commit log for simplicity. - // PRO: No syncronization needed for the enumerator. - // CON: May take longer for the initial load and use more memory. - // For reference, I tested on my dev machine on a repo with an *enormous* number of commits - // https://github.com/python/cpython with > 120k commits. This was a one-time cost of 2-3 seconds, but also - // consumed several hundred MB of memory. - - // Often, but not always, the root folder has some boilerplate/doc/config that rarely changes - // Therefore, populating the last commit for each file in the root folder often requires a large portion of the commit history anyway. - // This somewhat blunts the appeal of trying to load this incrementally. - _commits.AddRange(repo.Commits); + _workingDirectory = repo.Info.WorkingDirectory; + + // Use the command line to get the last commit for a file, on demand. + // PRO: If Git is installed, this will always succeed, and in a somewhat predictable amount of time. + // Doesn't consume memory for the entire commit log. + // CON: Spawning a process for each file may be slower than walking to recent commits. + // Does not work if Git isn't installed. + if (_preferCommandLine) + { + _gitInstalled = _gitDetect.DetectGit(); + _useCommandLine = _gitInstalled; + } + + if (!_useCommandLine) + { + // Greedily get the entire commit log for simplicity. + // PRO: No syncronization needed for the enumerator. + // CON: May take longer for the initial load and use more memory. + // For reference, I tested on my dev machine on a repo with an *large* number of commits + // https://github.com/python/cpython with > 120k commits. This was a one-time cost of 2-3 seconds, but also + // consumed several hundred MB of memory. + // Unfortunately, loading an *enormous* repo with 1M+ commits consumes a multiple GBs of memory. + + // For smaller repos this method is faster, but the memory consumption is prohibitive on the huge ones. + // Additionally, virtualized repos (aka GVFS) may show the entire commit log, but each commit's tree isn't always hydrated. + // As a result, GVFS repos often fail to find the last commit for a file if it is older than some unknown threshold. + + // Often, but not always, the root folder has some boilerplate/doc/config that rarely changes + // Therefore, populating the last commit for each file in the root folder often requires a large portion of the commit history anyway. + // This somewhat blunts the appeal of trying to load this incrementally. + _commits.AddRange(repo.Commits); + } } - public IEnumerator GetEnumerator() + public CommitWrapper? FindLastCommit(string relativePath) { - return _commits.GetEnumerator(); + if (_cache.TryGetValue(relativePath, out var cachedCommit)) + { + return cachedCommit; + } + + CommitWrapper? result; + if (_useCommandLine) + { + result = FindLastCommitUsingCommandLine(relativePath); + } + else + { + result = FindLastCommitUsingLibGit2Sharp(relativePath); + } + + if (result != null) + { + result = _cache.GetOrAdd(relativePath, result); + } + + return result; } - IEnumerator IEnumerable.GetEnumerator() + private CommitWrapper? FindLastCommitUsingCommandLine(string relativePath) { - return GetEnumerator(); + var result = GitExecute.ExecuteGitCommand(_gitDetect.GitConfiguration.ReadInstallPath(), _workingDirectory, $"log -n 1 --pretty=format:%s%n%an%n%ae%n%aI%n%H -- {relativePath}"); + if ((result.Status != Microsoft.Windows.DevHome.SDK.ProviderOperationStatus.Success) || (result.Output is null)) + { + return null; + } + + var parts = result.Output.Split('\n'); + string message = parts[0]; + string authorName = parts[1]; + string authorEmail = parts[2]; + DateTimeOffset authorWhen = DateTimeOffset.Parse(parts[3], null, System.Globalization.DateTimeStyles.RoundtripKind); + string sha = parts[4]; + return new CommitWrapper(message, authorName, authorEmail, authorWhen, sha); + } + + private CommitWrapper? FindLastCommitUsingLibGit2Sharp(string relativePath) + { + var checkedFirstCommit = false; + foreach (var currentCommit in _commits) + { + // Now that CommitLogCache is caching the result of the revwalk, the next piece that is most expensive + // is obtaining relativePath's TreeEntry from the Tree (e.g. currentTree[relativePath]. + // Digging into the git shows that number of directory entries and/or directory depth may play a factor. + // There may also be a lot of redundant lookups happening here, so it may make sense to do some LRU caching. + var currentTree = currentCommit.Tree; + var currentTreeEntry = currentTree[relativePath]; + if (currentTreeEntry == null) + { + if (checkedFirstCommit) + { + continue; + } + else + { + // If this file isn't present in the most recent commit, then it's of no interest + return null; + } + } + + checkedFirstCommit = true; + var parents = currentCommit.Parents; + var count = parents.Count(); + if (count == 0) + { + return new CommitWrapper(currentCommit); + } + else if (count > 1) + { + // Multiple parents means a merge. Ignore. + continue; + } + else + { + var parentTree = parents.First(); + var parentTreeEntry = parentTree[relativePath]; + if (parentTreeEntry == null || parentTreeEntry.Target.Id != currentTreeEntry.Target.Id) + { + return new CommitWrapper(currentCommit); + } + } + } + + return null; } } diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitWrapper.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitWrapper.cs new file mode 100644 index 0000000000..a025577139 --- /dev/null +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/CommitWrapper.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using LibGit2Sharp; + +internal sealed class CommitWrapper +{ + public string MessageShort { get; private set; } + + public string AuthorName { get; private set; } + + public string AuthorEmail { get; private set; } + + public DateTimeOffset AuthorWhen { get; private set; } + + public string Sha { get; private set; } + + public CommitWrapper(Commit commit) + { + MessageShort = commit.MessageShort; + AuthorName = commit.Author.Name; + AuthorEmail = commit.Author.Email; + AuthorWhen = commit.Author.When; + Sha = commit.Sha; + } + + public CommitWrapper(string messageShort, string authorName, string authorEmail, DateTimeOffset authorWhen, string sha) + { + MessageShort = messageShort; + AuthorName = authorName; + AuthorEmail = authorEmail; + AuthorWhen = authorWhen; + Sha = sha; + } +} diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitExecute.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitExecute.cs index ba023813d3..4b625f30d7 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitExecute.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitExecute.cs @@ -23,6 +23,7 @@ public static GitCommandRunnerResultInfo ExecuteGitCommand(string gitApplication UseShellExecute = false, CreateNoWindow = true, WorkingDirectory = repositoryDirectory ?? string.Empty, + StandardOutputEncoding = System.Text.Encoding.UTF8, }; using var process = Process.Start(processStartInfo); diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitLocalRepository.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitLocalRepository.cs index c4dae32b75..61e24afe37 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/GitLocalRepository.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/GitLocalRepository.cs @@ -57,11 +57,11 @@ IPropertySet ILocalRepository.GetProperties(string[] properties, string relative { relativePath = relativePath.Replace('\\', '/'); var result = new ValueSet(); - Commit? latestCommit = null; + CommitWrapper? latestCommit = null; var repository = OpenRepository(); - if (repository == null) + if (repository is null) { _log.Debug("GetProperties: Repository object is null"); return result; @@ -72,89 +72,54 @@ IPropertySet ILocalRepository.GetProperties(string[] properties, string relative switch (propName) { case "System.VersionControl.LastChangeMessage": - if (latestCommit == null) + latestCommit ??= FindLatestCommit(relativePath, repository); + if (latestCommit is not null) { - latestCommit = FindLatestCommit(relativePath, repository); - if (latestCommit != null) - { - result.Add("System.VersionControl.LastChangeMessage", latestCommit.MessageShort); - } - } - else - { - result.Add("System.VersionControl.LastChangeMessage", latestCommit.MessageShort); + result.Add(propName, latestCommit.MessageShort); } break; case "System.VersionControl.LastChangeAuthorName": - if (latestCommit == null) - { - latestCommit = FindLatestCommit(relativePath, repository); - if (latestCommit != null) - { - result.Add("System.VersionControl.LastChangeAuthorName", latestCommit.Author.Name); - } - } - else + latestCommit ??= FindLatestCommit(relativePath, repository); + if (latestCommit is not null) { - result.Add("System.VersionControl.LastChangeAuthorName", latestCommit.Author.Name); + result.Add(propName, latestCommit.AuthorName); } break; case "System.VersionControl.LastChangeDate": - if (latestCommit == null) - { - latestCommit = FindLatestCommit(relativePath, repository); - if (latestCommit != null) - { - result.Add("System.VersionControl.LastChangeDate", latestCommit.Author.When); - } - } - else + latestCommit ??= FindLatestCommit(relativePath, repository); + if (latestCommit is not null) { - result.Add("System.VersionControl.LastChangeDate", latestCommit.Author.When); + result.Add(propName, latestCommit.AuthorWhen); } break; case "System.VersionControl.LastChangeAuthorEmail": - if (latestCommit == null) + latestCommit ??= FindLatestCommit(relativePath, repository); + if (latestCommit is not null) { - latestCommit = FindLatestCommit(relativePath, repository); - if (latestCommit != null) - { - result.Add("System.VersionControl.LastChangeAuthorEmail", latestCommit.Author.Email); - } - } - else - { - result.Add("System.VersionControl.LastChangeAuthorEmail", latestCommit.Author.Email); + result.Add(propName, latestCommit.AuthorEmail); } break; case "System.VersionControl.LastChangeID": - if (latestCommit == null) + latestCommit ??= FindLatestCommit(relativePath, repository); + if (latestCommit is not null) { - latestCommit = FindLatestCommit(relativePath, repository); - if (latestCommit != null) - { - result.Add("System.VersionControl.LastChangeID", latestCommit.Sha); - } - } - else - { - result.Add("System.VersionControl.LastChangeID", latestCommit.Sha); + result.Add(propName, latestCommit.Sha); } break; case "System.VersionControl.Status": - result.Add("System.VersionControl.Status", GetStatus(relativePath, repository)); + result.Add(propName, GetStatus(relativePath, repository)); break; case "System.VersionControl.CurrentFolderStatus": var folderStatus = GetFolderStatus(relativePath, repository); - if (folderStatus != null) + if (folderStatus is not null) { - result.Add("System.VersionControl.CurrentFolderStatus", folderStatus); + result.Add(propName, folderStatus); } break; @@ -194,53 +159,8 @@ public IPropertySet GetProperties(string[] properties, string relativePath) } } - private Commit? FindLatestCommit(string relativePath, RepositoryWrapper repository) + private CommitWrapper? FindLatestCommit(string relativePath, RepositoryWrapper repository) { - var checkedFirstCommit = false; - foreach (var currentCommit in repository.GetCommits()) - { - // Now that CommitLogCache is caching the result of the revwalk, the next piece that is most expensive - // is obtaining relativePath's TreeEntry from the Tree (e.g. currentTree[relativePath]. - // Digging into the git shows that number of directory entries and/or directory depth may play a factor. - // There may also be a lot of redundant lookups happening here, so it may make sense to do some LRU caching. - var currentTree = currentCommit.Tree; - var currentTreeEntry = currentTree[relativePath]; - if (currentTreeEntry == null) - { - if (checkedFirstCommit) - { - continue; - } - else - { - // If this file isn't present in the most recent commit, then it's of no interest - return null; - } - } - - checkedFirstCommit = true; - var parents = currentCommit.Parents; - var count = parents.Count(); - if (count == 0) - { - return currentCommit; - } - else if (count > 1) - { - // Multiple parents means a merge. Ignore. - continue; - } - else - { - var parentTree = parents.First(); - var parentTreeEntry = parentTree[relativePath]; - if (parentTreeEntry == null || parentTreeEntry.Target.Id != currentTreeEntry.Target.Id) - { - return currentCommit; - } - } - } - - return null; + return repository.FindLastCommit(relativePath); } } diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/LruCacheDictionary.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/LruCacheDictionary.cs new file mode 100644 index 0000000000..870fe07e8d --- /dev/null +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/LruCacheDictionary.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using Newtonsoft.Json.Linq; + +namespace FileExplorerGitIntegration.Models; + +// A simple LRU cache dictionary implementation. +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "File names for generics are ugly.")] +internal sealed class LruCacheDictionary + where TKey : notnull +{ + private readonly int _capacity; + private const int DefaultCapacity = 512; + private readonly object _lock = new(); + private readonly Dictionary> _dict; + private readonly LinkedList<(TKey, TValue)> _lruList = new(); + + public LruCacheDictionary(int capacity = DefaultCapacity) + { + _capacity = capacity; + _dict = []; + } + + public bool TryGetValue(TKey key, out TValue value) + { + lock (_lock) + { + if (_dict.TryGetValue(key, out var node)) + { + RenewExistingNodeNoLock(node); + value = node.Value.Item2; + return true; + } + + value = default!; + return false; + } + } + + public TValue GetOrAdd(TKey key, TValue value) + { + lock (_lock) + { + if (_dict.TryGetValue(key, out var node)) + { + RenewExistingNodeNoLock(node); + return node.Value.Item2; + } + + AddAndTrimNoLock(key, value); + return value; + } + } + + private void AddAndTrimNoLock(TKey key, TValue value) + { + var newNode = new LinkedListNode<(TKey, TValue)>((key, value)); + AddNewNodeNoLock(newNode); + TrimToCapacityNoLock(); + } + + private void RenewExistingNodeNoLock(LinkedListNode<(TKey, TValue)> node) + { + Debug.Assert(node.List == _lruList, "Node is not in the list"); + _lruList.Remove(node); + _lruList.AddLast(node); + } + + private void AddNewNodeNoLock(LinkedListNode<(TKey, TValue)> node) + { + Debug.Assert(node.List == null, "Node is already in the list"); + _lruList.AddLast(node); + _dict.Add(node.Value.Item1, node); + } + + private void TrimToCapacityNoLock() + { + if (_lruList.Count > _capacity) + { + var node = _lruList.First; + if (node != null) + { + _lruList.RemoveFirst(); + _dict.Remove(node.Value.Item1); + } + } + } +} diff --git a/extensions/GitExtension/FileExplorerGitIntegration/Models/RepositoryWrapper.cs b/extensions/GitExtension/FileExplorerGitIntegration/Models/RepositoryWrapper.cs index 83d92b6907..e1ae4368c8 100644 --- a/extensions/GitExtension/FileExplorerGitIntegration/Models/RepositoryWrapper.cs +++ b/extensions/GitExtension/FileExplorerGitIntegration/Models/RepositoryWrapper.cs @@ -30,7 +30,13 @@ public RepositoryWrapper(string rootFolder) _statusCache = new StatusCache(rootFolder); } - public IEnumerable GetCommits() + public CommitWrapper? FindLastCommit(string relativePath) + { + var commitLog = GetCommitLogCache(); + return commitLog.FindLastCommit(relativePath); + } + + private CommitLogCache GetCommitLogCache() { // Fast path: if we have an up-to-date commit log, return that if (_head != null && _commits != null) From f57de599f309946fa9e879014a7577155320148c Mon Sep 17 00:00:00 2001 From: Ryan Shepherd Date: Thu, 8 Aug 2024 13:11:53 -0700 Subject: [PATCH 68/82] Define Stable and Canary build constants universally (#3574) * Define stable and canary build constants universally * Remove duplicate BuildRing logic from sub-projects --- Directory.Build.props | 3 +++ common/DevHome.Common.csproj | 5 ----- .../CoreWidgetProvider/CoreWidgetProvider.csproj | 5 ----- .../src/DevSetupAgent/DevSetupAgent.csproj | 7 ------- extensions/WSLExtension/WSLExtension.csproj | 15 +++++---------- src/DevHome.csproj | 5 ----- .../DevHome.Dashboard/DevHome.Dashboard.csproj | 5 ----- .../DevHome.SetupFlow.ElevatedServer.csproj | 1 - 8 files changed, 8 insertions(+), 38 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 70daf3ff7f..93f4387ff5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -28,6 +28,9 @@ <_PropertySheetDisplayName>DevHome.Root.Props $(MsbuildThisFileDirectory)\Cpp.Build.props + Dev + $(DefineConstants);CANARY_BUILD + $(DefineConstants);STABLE_BUILD diff --git a/common/DevHome.Common.csproj b/common/DevHome.Common.csproj index 729efe849c..0186c91be6 100644 --- a/common/DevHome.Common.csproj +++ b/common/DevHome.Common.csproj @@ -113,9 +113,4 @@ Always - - - $(DefineConstants);CANARY_BUILD - $(DefineConstants);STABLE_BUILD - diff --git a/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj b/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj index f840ae97fa..cfe0ff20db 100644 --- a/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj +++ b/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj @@ -21,11 +21,6 @@ $(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml - - $(DefineConstants);CANARY_BUILD - $(DefineConstants);STABLE_BUILD - - diff --git a/extensions/HyperVExtension/src/DevSetupAgent/DevSetupAgent.csproj b/extensions/HyperVExtension/src/DevSetupAgent/DevSetupAgent.csproj index 8318ef70a6..a4388b248c 100644 --- a/extensions/HyperVExtension/src/DevSetupAgent/DevSetupAgent.csproj +++ b/extensions/HyperVExtension/src/DevSetupAgent/DevSetupAgent.csproj @@ -4,7 +4,6 @@ enable enable dotnet-DevSetupAgent-674f51cd-70a6-4b78-8376-66efbf84c412 - Dev x86;x64;arm64 win-x86;win-x64;win-arm64 Properties\PublishProfiles\win-$(Platform).pubxml @@ -28,12 +27,6 @@ win-arm64 - - - $(DefineConstants);CANARY_BUILD - $(DefineConstants);STABLE_BUILD - - diff --git a/extensions/WSLExtension/WSLExtension.csproj b/extensions/WSLExtension/WSLExtension.csproj index a12e1b6b80..a13d4ede91 100644 --- a/extensions/WSLExtension/WSLExtension.csproj +++ b/extensions/WSLExtension/WSLExtension.csproj @@ -12,20 +12,15 @@ WinExe - + enable enable x86;x64;arm64 win-x86;win-x64;win-arm64 WSLExtension.Program - WSLExtensionServer - $(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml - - - Dev - $(DefineConstants);CANARY_BUILD - $(DefineConstants);STABLE_BUILD + WSLExtensionServer + $(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml @@ -45,8 +40,8 @@ - - + + diff --git a/src/DevHome.csproj b/src/DevHome.csproj index 72687ab4c8..96e45a4257 100644 --- a/src/DevHome.csproj +++ b/src/DevHome.csproj @@ -164,11 +164,6 @@ - - $(DefineConstants);CANARY_BUILD - $(DefineConstants);STABLE_BUILD - - $(DefineConstants);DEBUG diff --git a/tools/Dashboard/DevHome.Dashboard/DevHome.Dashboard.csproj b/tools/Dashboard/DevHome.Dashboard/DevHome.Dashboard.csproj index bbd8dee365..da920fd2d5 100644 --- a/tools/Dashboard/DevHome.Dashboard/DevHome.Dashboard.csproj +++ b/tools/Dashboard/DevHome.Dashboard/DevHome.Dashboard.csproj @@ -71,9 +71,4 @@ $(DefineConstants);DEBUG;DEBUG_FAILFAST - - - $(DefineConstants);CANARY_BUILD - $(DefineConstants);STABLE_BUILD - diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedServer/DevHome.SetupFlow.ElevatedServer.csproj b/tools/SetupFlow/DevHome.SetupFlow.ElevatedServer/DevHome.SetupFlow.ElevatedServer.csproj index 5d6f4903f2..51097a8e4a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedServer/DevHome.SetupFlow.ElevatedServer.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedServer/DevHome.SetupFlow.ElevatedServer.csproj @@ -5,7 +5,6 @@ Exe - Dev $(SolutionDir)\src\Assets\Dev\DevHome_Dev.ico $(SolutionDir)\src\Assets\Canary\DevHome_Canary.ico $(SolutionDir)\src\Assets\Preview\DevHome_Preview.ico From c6265385af43d1b1c1800707c554e527b0448a75 Mon Sep 17 00:00:00 2001 From: Lauren Ciha <64796985+lauren-ciha@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:22:15 -0700 Subject: [PATCH 69/82] Add heading levels to Extensions page (#3505) * Add group narration to extensions page * Removed IsTabStop from non-interactive headers * Add heading levels * Removed unnecessary AutomationsProperty --- .../Strings/en-us/Resources.resw | 65 ++++++++ .../Views/ExtensionLibraryView.xaml | 148 +++++++++--------- 2 files changed, 142 insertions(+), 71 deletions(-) diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Strings/en-us/Resources.resw b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Strings/en-us/Resources.resw index 4aaa52c717..9f0205bafa 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Strings/en-us/Resources.resw +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Strings/en-us/Resources.resw @@ -1,5 +1,64 @@  + @@ -134,4 +193,10 @@ More options ToolTip of the button that brings up the "More options" menu + + Installed Items Stack Panel Name + + + Available in the Microsoft Store Automation Properties Name + \ No newline at end of file diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml index 347f7089b3..d1b99b9534 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml @@ -48,107 +48,113 @@ Margin="0,0,0,28" /> - - + + - - - - - + + + + + + + - - - - - - - - - - + + + + + + + + + - + + - - - - - - - - - + + + + + + + - + - + diff --git a/tools/PI/DevHome.PI/Strings/en-us/Resources.resw b/tools/PI/DevHome.PI/Strings/en-us/Resources.resw index c13aad6807..013d83431e 100644 --- a/tools/PI/DevHome.PI/Strings/en-us/Resources.resw +++ b/tools/PI/DevHome.PI/Strings/en-us/Resources.resw @@ -317,6 +317,10 @@ Clear the list Tooltip for a button that will clear the log list. + + Time Generated + The time at which the log was generated. + Category A header for what category the log entry belongs. diff --git a/tools/PI/DevHome.PI/ViewModels/WinLogsPageViewModel.cs b/tools/PI/DevHome.PI/ViewModels/WinLogsPageViewModel.cs index db01563ed4..762389b9c9 100644 --- a/tools/PI/DevHome.PI/ViewModels/WinLogsPageViewModel.cs +++ b/tools/PI/DevHome.PI/ViewModels/WinLogsPageViewModel.cs @@ -9,6 +9,7 @@ using System.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.WinUI.Collections; using CommunityToolkit.WinUI.UI.Controls; using DevHome.Common.Extensions; using DevHome.Common.Helpers; @@ -33,6 +34,9 @@ public partial class WinLogsPageViewModel : ObservableObject, IDisposable [ObservableProperty] private ObservableCollection _winLogEntries; + [ObservableProperty] + private AdvancedCollectionView _winLogsView; + [ObservableProperty] private Visibility _runAsAdminVisibility = Visibility.Collapsed; @@ -51,6 +55,9 @@ public partial class WinLogsPageViewModel : ObservableObject, IDisposable [ObservableProperty] private bool _isWEREnabled = true; + [ObservableProperty] + private string _filterMessageText; + private Process? _targetProcess; private WinLogsHelper? _winLogsHelper; @@ -65,9 +72,13 @@ public WinLogsPageViewModel() _insightsService = Application.Current.GetService(); + _filterMessageText = string.Empty; _winLogEntries = []; _winLogsOutput = []; _winLogsOutput.CollectionChanged += WinLogsOutput_CollectionChanged; + _winLogsView = new AdvancedCollectionView(_winLogEntries, true); + _winLogsView.SortDescriptions.Add(new SortDescription(nameof(WinLogsEntry.TimeGenerated), SortDirection.Ascending)); + _winLogsView.Filter = entry => string.IsNullOrEmpty(FilterMessageText) || ((WinLogsEntry)entry).Message.Contains(FilterMessageText, StringComparison.CurrentCultureIgnoreCase); var process = TargetAppData.Instance.TargetProcess; if (process is not null) @@ -232,4 +243,10 @@ private void RunAsAdmin() CommonHelper.RunAsAdmin(_targetProcess.Id, nameof(WinLogsPageViewModel)); } } + + [RelayCommand] + private void UpdateWinLogs() + { + WinLogsView.RefreshFilter(); + } } From 22b3359a933aef32a594e1122a4c3b5d8c3b9d5b Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:00:51 -0700 Subject: [PATCH 75/82] Fixing accessibility bugs (#3580) --- .../Services/StringResourceKey.cs | 1 + .../Strings/en-us/Resources.resw | 12 ++++------ .../Styles/AppManagement_ThemeResources.xaml | 3 ++- .../ViewModels/PackageViewModel.cs | 2 ++ .../Views/AppManagementView.xaml | 2 +- .../Views/ConfigurationFileView.xaml | 1 + .../Views/PackageCatalogListView.xaml | 2 +- .../DevHome.SetupFlow/Views/PackageView.xaml | 5 ++-- .../DevHome.SetupFlow/Views/SearchView.xaml | 12 +++++----- .../Views/SearchView.xaml.cs | 14 ----------- .../Summary/SummaryShowAppsAndRepos.xaml | 5 +--- .../Summary/SummaryShowAppsAndRepos.xaml.cs | 23 ++++++++++++++++++- 12 files changed, 44 insertions(+), 38 deletions(-) diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs index 71ffcf04b9..b89cb03eb7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs @@ -70,6 +70,7 @@ public static class StringResourceKey public static readonly string PackageDescriptionTwoParts = nameof(PackageDescriptionTwoParts); public static readonly string PackageInstalledTooltip = nameof(PackageInstalledTooltip); public static readonly string PackageNameTooltip = nameof(PackageNameTooltip); + public static readonly string PackageInstalledAnnouncement = nameof(PackageInstalledAnnouncement); public static readonly string PackagePublisherNameTooltip = nameof(PackagePublisherNameTooltip); public static readonly string PackageSourceTooltip = nameof(PackageSourceTooltip); public static readonly string PackageVersionTooltip = nameof(PackageVersionTooltip); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw index 35b62e934a..fc7ee75c81 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -625,6 +625,10 @@ Already installed Label for a package/application that already exists on the user's machine. + + {0}. Already installed + {Locked="{0}","."}Text announced for a package/application that already exists on the user's machine. {0} is replaced by the package name. A period (.) is used to add a pause for the narrator. + Name: {0} {Locked="{0}"} Label for a package name displayed on a tooltip. {0} is replaced by the package name. @@ -2007,14 +2011,6 @@ Error: {0} Locked={"{0}"} Error message with additional details in {0} shown when we failed to generate the project. - - Version - Name for the version combo box - - - Version - Name for the version combo box - Installation Notes Text to introduce installation notes in the summary screen. diff --git a/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml b/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml index da05f591b5..a8197fbde3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml @@ -81,7 +81,8 @@ Name; + public string PackageAnnouncement => IsInstalled ? _stringResource.GetLocalized(StringResourceKey.PackageInstalledAnnouncement, Name) : Name; + public string TooltipName => _stringResource.GetLocalized(StringResourceKey.PackageNameTooltip, Name); public string TooltipVersion => _stringResource.GetLocalized(StringResourceKey.PackageVersionTooltip, SelectedVersion); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml index a187dd6aa3..7b0ba12626 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml @@ -180,7 +180,7 @@ - + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/ConfigurationFileView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/ConfigurationFileView.xaml index 15442d1e06..822bb729c7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/ConfigurationFileView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/ConfigurationFileView.xaml @@ -59,6 +59,7 @@ + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/PackageCatalogListView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/PackageCatalogListView.xaml index ff9bbb49b2..640c7173cf 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/PackageCatalogListView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/PackageCatalogListView.xaml @@ -69,7 +69,7 @@ - diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/PackageView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/PackageView.xaml index 9ddd6af640..9d026efe2c 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/PackageView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/PackageView.xaml @@ -6,7 +6,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:converters="using:CommunityToolkit.WinUI.Converters" xmlns:controls="using:DevHome.SetupFlow.Controls" - AutomationProperties.Name="{Binding PackageTitle}" + AutomationProperties.Name="{Binding PackageAnnouncement}" mc:Ignorable="d"> @@ -104,7 +104,8 @@ - - - + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SearchView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/SearchView.xaml.cs index 24edb435c9..4f97afd2a1 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/SearchView.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SearchView.xaml.cs @@ -16,18 +16,4 @@ public SearchView() { this.InitializeComponent(); } - - private void PackagesListView_Loaded(object sender, RoutedEventArgs e) - { - if (sender is ListView listView) - { - for (var i = 0; i < listView.Items.Count; i++) - { - if (listView.ContainerFromIndex(i) is ListViewItem item && item.Content is PackageViewModel package) - { - AutomationProperties.SetName(item, package.PackageTitle); - } - } - } - } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Summary/SummaryShowAppsAndRepos.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Summary/SummaryShowAppsAndRepos.xaml index 288d0a8a66..8e3d8845a6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/Summary/SummaryShowAppsAndRepos.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Summary/SummaryShowAppsAndRepos.xaml @@ -105,7 +105,7 @@ Padding="0,12" Style="{ThemeResource BodyStrongTextBlockStyle}" /> - + @@ -117,9 +117,6 @@ Width="250" Padding="5" AutomationProperties.Name="{x:Bind PackageTitle}"> - - - diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Summary/SummaryShowAppsAndRepos.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/Summary/SummaryShowAppsAndRepos.xaml.cs index 7ffee33720..2f68d8d6bf 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/Summary/SummaryShowAppsAndRepos.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Summary/SummaryShowAppsAndRepos.xaml.cs @@ -3,6 +3,7 @@ using AdaptiveCards.Rendering.WinUI3; using CommunityToolkit.Mvvm.Messaging; +using DevHome.SetupFlow.Controls; using DevHome.SetupFlow.Models.Environments; using DevHome.SetupFlow.ViewModels; using Microsoft.UI.Xaml; @@ -63,5 +64,25 @@ private void AddAdaptiveCardToUI(RenderedAdaptiveCard renderedAdaptiveCard) AdaptiveCardGrid.Children.Clear(); AdaptiveCardGrid.Children.Add(frameworkElement); - } + } + + private void PackagesGridView_Loaded(object sender, RoutedEventArgs e) + { + if (sender is not GridView gridView) + { + return; + } + + // Set the tooltip for each item in the grid view + for (var i = 0; i < gridView.Items.Count; i++) + { + if (gridView.ContainerFromIndex(i) is GridViewItem item && item.Content is PackageViewModel packageViewModel) + { + ToolTipService.SetToolTip(item, new PackageDetailsTooltip() + { + Package = packageViewModel, + }); + } + } + } } From 61c2a5d83e75f9037f211f0ef3dd1cb72d8ab258 Mon Sep 17 00:00:00 2001 From: Kristen Schau <47155823+krschau@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:00:30 -0400 Subject: [PATCH 76/82] Cancel Dashboard loading when navigating away (#3587) --- .../Views/DashboardView.xaml.cs | 90 +++++++++++++++---- 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs index 714912a10a..d04c373ea5 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs @@ -52,6 +52,7 @@ public partial class DashboardView : ToolPage, IDisposable private static DispatcherQueue _dispatcherQueue; private readonly ILocalSettingsService _localSettingsService; private readonly IWidgetExtensionService _widgetExtensionService; + private CancellationTokenSource _initWidgetsCancellationTokenSource; private bool _disposedValue; private const string DraggedWidget = "DraggedWidget"; @@ -143,6 +144,8 @@ private async Task OnLoadedAsync() [RelayCommand] private async Task OnUnloadedAsync() { + _log.Debug($"Unloading Dashboard, cancel any loading."); + _initWidgetsCancellationTokenSource.Cancel(); ViewModel.PinnedWidgets.CollectionChanged -= OnPinnedWidgetsCollectionChangedAsync; Bindings.StopTracking(); @@ -150,16 +153,17 @@ private async Task OnUnloadedAsync() _log.Debug($"Leaving Dashboard, deactivating widgets."); + await _pinnedWidgetsLock.WaitAsync(); try { await Task.Run(() => UnsubscribeFromWidgets()); } - catch (Exception ex) + finally { - _log.Error(ex, "Exception in UnsubscribeFromWidgets:"); + ViewModel.PinnedWidgets.Clear(); + _pinnedWidgetsLock.Release(); } - ViewModel.PinnedWidgets.Clear(); await UnsubscribeFromWidgetCatalogEventsAsync(); } @@ -200,7 +204,16 @@ private async Task InitializeDashboard() await _localSettingsService.SaveSettingAsync(WellKnownSettingsKeys.IsNotFirstDashboardRun, true); } - await InitializePinnedWidgetListAsync(isFirstDashboardRun); + _initWidgetsCancellationTokenSource = new(); + try + { + await InitializePinnedWidgetListAsync(isFirstDashboardRun, _initWidgetsCancellationTokenSource.Token); + } + catch (OperationCanceledException ex) + { + _log.Information(ex, "InitializePinnedWidgetListAsync operation was cancelled."); + return; + } } else { @@ -229,7 +242,7 @@ private async Task InitializeDashboard() ViewModel.IsLoading = false; } - private async Task InitializePinnedWidgetListAsync(bool isFirstDashboardRun) + private async Task InitializePinnedWidgetListAsync(bool isFirstDashboardRun, CancellationToken cancellationToken) { var hostWidgets = await GetPreviouslyPinnedWidgets(); if ((hostWidgets.Length == 0) && isFirstDashboardRun) @@ -237,11 +250,40 @@ private async Task InitializePinnedWidgetListAsync(bool 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(); + await _pinnedWidgetsLock.WaitAsync(CancellationToken.None); + try + { + await PinDefaultWidgetsAsync(cancellationToken); + } + catch (OperationCanceledException ex) + { + // If the operation is cancelled, delete any default widgets that were already pinned and reset the IsNotFirstDashboardRun setting. + // Next time the user opens the Dashboard, treat it as the first run again. + _log.Information(ex, "PinDefaultWidgetsAsync operation was cancelled, delete any widgets already pinned"); + foreach (var widget in ViewModel.PinnedWidgets) + { + await widget.Widget.DeleteAsync(); + } + + await _localSettingsService.SaveSettingAsync(WellKnownSettingsKeys.IsNotFirstDashboardRun, false); + } + finally + { + _pinnedWidgetsLock.Release(); + } } else { - await RestorePinnedWidgetsAsync(hostWidgets); + await _pinnedWidgetsLock.WaitAsync(CancellationToken.None); + try + { + await RestorePinnedWidgetsAsync(hostWidgets, cancellationToken); + } + finally + { + // No cleanup to do if the operation is cancelled. + _pinnedWidgetsLock.Release(); + } } } @@ -274,7 +316,7 @@ private async Task GetPreviouslyPinnedWidgets() return [.. comSafeHostWidgets]; } - private async Task RestorePinnedWidgetsAsync(ComSafeWidget[] hostWidgets) + private async Task RestorePinnedWidgetsAsync(ComSafeWidget[] hostWidgets, CancellationToken cancellationToken) { var restoredWidgetsWithPosition = new SortedDictionary(); var restoredWidgetsWithoutPosition = new SortedDictionary(); @@ -289,6 +331,8 @@ private async Task RestorePinnedWidgetsAsync(ComSafeWidget[] hostWidgets) // append it at the end. If a position is missing, just show the next widget in order. foreach (var widget in hostWidgets) { + cancellationToken.ThrowIfCancellationRequested(); + try { var stateStr = await widget.GetCustomStateAsync(); @@ -381,7 +425,9 @@ private async Task RestorePinnedWidgetsAsync(ComSafeWidget[] hostWidgets) { var comSafeWidget = orderedWidget.Value; var size = await comSafeWidget.GetSizeAsync(); - await InsertWidgetInPinnedWidgetsAsync(comSafeWidget, size, finalPlace++); + cancellationToken.ThrowIfCancellationRequested(); + + await InsertWidgetInPinnedWidgetsAsync(comSafeWidget, size, finalPlace++, cancellationToken); } // Go through the newly created list of pinned widgets and update any positions that may have changed. @@ -390,6 +436,8 @@ private async Task RestorePinnedWidgetsAsync(ComSafeWidget[] hostWidgets) var updatedPlace = 0; foreach (var widget in ViewModel.PinnedWidgets) { + cancellationToken.ThrowIfCancellationRequested(); + await WidgetHelpers.SetPositionCustomStateAsync(widget.Widget, updatedPlace++); } @@ -410,21 +458,23 @@ private async Task DeleteAbandonedWidgetAsync(ComSafeWidget widget) _log.Information($"After delete, {length} widgets for this host"); } - private async Task PinDefaultWidgetsAsync() + private async Task PinDefaultWidgetsAsync(CancellationToken cancellationToken) { var comSafeWidgetDefinitions = await ComSafeHelpers.GetAllOrderedComSafeWidgetDefinitions(ViewModel.WidgetHostingService); foreach (var comSafeWidgetDefinition in comSafeWidgetDefinitions) { + cancellationToken.ThrowIfCancellationRequested(); + var id = comSafeWidgetDefinition.Id; if (WidgetHelpers.DefaultWidgetDefinitionIds.Contains(id)) { _log.Information($"Found default widget {id}"); - await PinDefaultWidgetAsync(comSafeWidgetDefinition); + await PinDefaultWidgetAsync(comSafeWidgetDefinition, cancellationToken); } } } - private async Task PinDefaultWidgetAsync(ComSafeWidgetDefinition defaultWidgetDefinition) + private async Task PinDefaultWidgetAsync(ComSafeWidgetDefinition defaultWidgetDefinition, CancellationToken cancellationToken) { try { @@ -467,9 +517,13 @@ private async Task PinDefaultWidgetAsync(ComSafeWidgetDefinition defaultWidgetDe await comSafeWidget.SetCustomStateAsync(newCustomState); // Put new widget on the Dashboard. - await InsertWidgetInPinnedWidgetsAsync(comSafeWidget, size, position); + await InsertWidgetInPinnedWidgetsAsync(comSafeWidget, size, position, cancellationToken); _log.Information($"Inserted default widget {unsafeWidgetId} at position {position}"); } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { // We can fail silently since this isn't in response to user action. @@ -580,9 +634,10 @@ await mainWindow.ShowErrorMessageDialogAsync( buttonText: stringResource.GetLocalized("CloseButtonText")); } - private async Task InsertWidgetInPinnedWidgetsAsync(ComSafeWidget widget, WidgetSize size, int index) + private async Task InsertWidgetInPinnedWidgetsAsync(ComSafeWidget widget, WidgetSize size, int index, CancellationToken cancellationToken = default) { - await Task.Run(async () => + await Task.Run( + async () => { var widgetDefinitionId = widget.DefinitionId; var widgetId = widget.Id; @@ -613,6 +668,8 @@ await Task.Run(async () => new ReportPinnedWidgetEvent(comSafeWidgetDefinition.ProviderDefinitionId, widgetDefinitionId)); var wvm = _widgetViewModelFactory(widget, size, comSafeWidgetDefinition); + cancellationToken.ThrowIfCancellationRequested(); + _dispatcherQueue.TryEnqueue(() => { try @@ -631,7 +688,8 @@ await Task.Run(async () => { await DeleteWidgetWithNoDefinition(widget, widgetDefinitionId); } - }); + }, + cancellationToken); } private async Task DeleteWidgetWithNoDefinition(ComSafeWidget widget, string widgetDefinitionId) From 3c86d4444b02dcc48a4088906003546c69025b20 Mon Sep 17 00:00:00 2001 From: Lauren Ciha <64796985+lauren-ciha@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:33:39 -0700 Subject: [PATCH 77/82] User/laurenciha/fix environment bug (#3588) * Update MoreOptions button name * Add back MoreOptionsButtonName resource * Remove button from name --- .../DevHome.Environments/Strings/en-us/Resources.resw | 4 ++++ .../DevHome.Environments/ViewModels/ComputeSystemCardBase.cs | 2 +- .../Environments/DevHome.Environments/Views/LandingPage.xaml | 3 +-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw index d43afe0231..505ba5870d 100644 --- a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw +++ b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw @@ -359,4 +359,8 @@ Set up Value to be shown in the button option to start the configuration flow + + More options + Name for MoreOptionsButton. It's listed as an additional string here because ComputeSystemCardBase can't access MoreOptionsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name (reason unknown) + \ No newline at end of file diff --git a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs index 2d004ec56b..a156405a9a 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs @@ -69,7 +69,7 @@ public abstract partial class ComputeSystemCardBase : ObservableObject public ComputeSystemCardBase() { - _moreOptionsButtonName = _stringResource.GetLocalized("MoreOptions.AutomationProperties.Name"); + _moreOptionsButtonName = _stringResource.GetLocalized("MoreOptionsButtonName"); } public override string ToString() diff --git a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml index 35442b4193..dfd7ed8b49 100644 --- a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml +++ b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml @@ -79,8 +79,7 @@