From e98c66535eff8eb93e883a160acbfccffb71e2ad Mon Sep 17 00:00:00 2001 From: Davide Giacometti Date: Fri, 10 May 2024 10:31:28 +0200 Subject: [PATCH 1/2] moved duplicate check in a dedicate service and made it async --- src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs | 1 + .../Hosts/Hosts/Settings/UserSettings.cs | 5 +- .../HostsUILib/Helpers/DuplicateService.cs | 164 ++++++++++++++++++ .../HostsUILib/Helpers/IDuplicateService.cs | 16 ++ .../Hosts/HostsUILib/Helpers/IHostsService.cs | 2 +- .../HostsUILib/Settings/IUserSettings.cs | 1 - .../HostsUILib/ViewModels/MainViewModel.cs | 131 ++------------ 7 files changed, 199 insertions(+), 121 deletions(-) create mode 100644 src/modules/Hosts/HostsUILib/Helpers/DuplicateService.cs create mode 100644 src/modules/Hosts/HostsUILib/Helpers/IDuplicateService.cs diff --git a/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs b/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs index 2d03c2ea280..f0a280baa64 100644 --- a/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs +++ b/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs @@ -47,6 +47,7 @@ public App() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Views and ViewModels services.AddSingleton(); diff --git a/src/modules/Hosts/Hosts/Settings/UserSettings.cs b/src/modules/Hosts/Hosts/Settings/UserSettings.cs index 42e2848c18a..8a3d9e844f6 100644 --- a/src/modules/Hosts/Hosts/Settings/UserSettings.cs +++ b/src/modules/Hosts/Hosts/Settings/UserSettings.cs @@ -5,7 +5,6 @@ using System; using System.IO.Abstractions; using System.Threading; -using HostsUILib.Helpers; using HostsUILib.Settings; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; @@ -45,6 +44,8 @@ public bool LoopbackDuplicates // Moved from Settings.UI.Library public HostsEncoding Encoding { get; set; } + public event EventHandler LoopbackDuplicatesChanged; + public UserSettings() { _settingsUtils = new SettingsUtils(); @@ -58,8 +59,6 @@ public UserSettings() _watcher = Helper.GetFileWatcher(HostsModuleName, "settings.json", () => LoadSettingsFromJson()); } - public event EventHandler LoopbackDuplicatesChanged; - private void LoadSettingsFromJson() { lock (_loadingSettingsLock) diff --git a/src/modules/Hosts/HostsUILib/Helpers/DuplicateService.cs b/src/modules/Hosts/HostsUILib/Helpers/DuplicateService.cs new file mode 100644 index 00000000000..51de3fd99fd --- /dev/null +++ b/src/modules/Hosts/HostsUILib/Helpers/DuplicateService.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using HostsUILib.Models; +using HostsUILib.Settings; +using Microsoft.UI.Dispatching; + +namespace HostsUILib.Helpers +{ + public class DuplicateService : IDuplicateService, IDisposable + { + private record struct Check(string Address, string[] Hosts); + + private readonly IUserSettings _userSettings; + private readonly DispatcherQueue _dispatcherQueue; + private readonly Queue _checkQueue; + private readonly ManualResetEvent _checkEvent; + private readonly Thread _queueThread; + + private readonly string[] _loopbackAddresses = + { + "0.0.0.0", + "::0", + "0:0:0:0:0:0:0:0", + "127.0.0.1", + "::1", + "0:0:0:0:0:0:0:1", + }; + + private ReadOnlyCollection _entries; + private bool _disposed; + + public DuplicateService(IUserSettings userSettings) + { + _userSettings = userSettings; + + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + _checkQueue = new Queue(); + _checkEvent = new ManualResetEvent(false); + + _queueThread = new Thread(ProcessQueue); + _queueThread.IsBackground = true; + _queueThread.Start(); + } + + public void Initialize(IList entries) + { + _entries = entries.AsReadOnly(); + + if (_checkQueue.Count > 0) + { + _checkQueue.Clear(); + } + + foreach (var entry in _entries) + { + if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address)) + { + continue; + } + + _checkQueue.Enqueue(new Check(entry.Address, entry.SplittedHosts)); + } + + _checkEvent.Set(); + } + + public void CheckDuplicates(string address, string[] hosts) + { + _checkQueue.Enqueue(new Check(address, hosts)); + _checkEvent.Set(); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private void ProcessQueue() + { + while (true) + { + _checkEvent.WaitOne(); + + while (_checkQueue.Count > 0) + { + var check = _checkQueue.Dequeue(); + FindDuplicates(check.Address, check.Hosts); + } + + _checkEvent.Reset(); + } + } + + private void FindDuplicates(string address, string[] hosts) + { + var entries = _entries.Where(e => + string.Equals(e.Address, address, StringComparison.OrdinalIgnoreCase) + || hosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any()); + + foreach (var entry in entries) + { + SetDuplicate(entry); + } + } + + private void SetDuplicate(Entry entry) + { + if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address)) + { + _dispatcherQueue.TryEnqueue(() => + { + entry.Duplicate = false; + }); + + return; + } + + var duplicate = false; + + /* + * Duplicate are based on the following criteria: + * Entries with the same type and at least one host in common + * Entries with the same type and address, except when there is only one entry with less than 9 hosts for that type and address + */ + if (_entries.Any(e => e != entry + && e.Type == entry.Type + && entry.SplittedHosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any())) + { + duplicate = true; + } + else if (_entries.Any(e => e != entry + && e.Type == entry.Type + && string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase))) + { + duplicate = entry.SplittedHosts.Length < Consts.MaxHostsCount + && _entries.Count(e => e.Type == entry.Type + && string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase) + && e.SplittedHosts.Length < Consts.MaxHostsCount) > 1; + } + + _dispatcherQueue.TryEnqueue(() => entry.Duplicate = duplicate); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _checkEvent?.Dispose(); + _disposed = true; + } + } + } + } +} diff --git a/src/modules/Hosts/HostsUILib/Helpers/IDuplicateService.cs b/src/modules/Hosts/HostsUILib/Helpers/IDuplicateService.cs new file mode 100644 index 00000000000..be9f842f990 --- /dev/null +++ b/src/modules/Hosts/HostsUILib/Helpers/IDuplicateService.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using HostsUILib.Models; + +namespace HostsUILib.Helpers +{ + public interface IDuplicateService + { + void Initialize(IList entries); + + void CheckDuplicates(string address, string[] hosts); + } +} diff --git a/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs b/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs index d97add2e7a0..955c1f8f8c6 100644 --- a/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs +++ b/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs @@ -9,7 +9,7 @@ namespace HostsUILib.Helpers { - public interface IHostsService : IDisposable + public interface IHostsService { string HostsFilePath { get; } diff --git a/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs b/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs index 13f1761ceb6..21a8e6fa36d 100644 --- a/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs +++ b/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Net; namespace HostsUILib.Settings { diff --git a/src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs b/src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs index 1a756d5124d..482bc7e1dd3 100644 --- a/src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs +++ b/src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs @@ -8,7 +8,6 @@ using System.IO; using System.Linq; using System.Linq.Expressions; -using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -23,21 +22,14 @@ namespace HostsUILib.ViewModels { - public partial class MainViewModel : ObservableObject, IDisposable + public partial class MainViewModel : ObservableObject { private readonly IHostsService _hostsService; private readonly IUserSettings _userSettings; + private readonly IDuplicateService _duplicateService; private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - private readonly string[] _loopbackAddresses = - { - "127.0.0.1", - "::1", - "0:0:0:0:0:0:0:1", - }; private bool _readingHosts; - private bool _disposed; - private CancellationTokenSource _tokenSource; [ObservableProperty] private Entry _selected; @@ -95,10 +87,16 @@ public partial class MainViewModel : ObservableObject, IDisposable private OpenSettingsFunction _openSettingsFunction; - public MainViewModel(IHostsService hostService, IUserSettings userSettings, ILogger logger, OpenSettingsFunction openSettingsFunction) + public MainViewModel( + IHostsService hostService, + IUserSettings userSettings, + IDuplicateService duplicateService, + ILogger logger, + OpenSettingsFunction openSettingsFunction) { _hostsService = hostService; _userSettings = userSettings; + _duplicateService = duplicateService; _hostsService.FileChanged += (s, e) => _dispatcherQueue.TryEnqueue(() => FileChanged = true); _userSettings.LoopbackDuplicatesChanged += (s, e) => ReadHosts(); @@ -111,8 +109,7 @@ public void Add(Entry entry) { entry.PropertyChanged += Entry_PropertyChanged; _entries.Add(entry); - - FindDuplicates(entry.Address, entry.SplittedHosts); + _duplicateService.CheckDuplicates(entry.Address, entry.SplittedHosts); } public void Update(int index, Entry entry) @@ -126,8 +123,8 @@ public void Update(int index, Entry entry) existingEntry.Hosts = entry.Hosts; existingEntry.Active = entry.Active; - FindDuplicates(oldAddress, oldHosts); - FindDuplicates(entry.Address, entry.SplittedHosts); + _duplicateService.CheckDuplicates(oldAddress, oldHosts); + _duplicateService.CheckDuplicates(entry.Address, entry.SplittedHosts); } public void DeleteSelected() @@ -135,8 +132,7 @@ public void DeleteSelected() var address = Selected.Address; var hosts = Selected.SplittedHosts; _entries.Remove(Selected); - - FindDuplicates(address, hosts); + _duplicateService.CheckDuplicates(address, hosts); } public void UpdateAdditionalLines(string lines) @@ -169,8 +165,7 @@ public void DeleteEntry(Entry entry) var address = entry.Address; var hosts = entry.SplittedHosts; _entries.Remove(entry); - - FindDuplicates(address, hosts); + _duplicateService.CheckDuplicates(address, hosts); } } @@ -213,9 +208,7 @@ public void ReadHosts() }); _readingHosts = false; - _tokenSource?.Cancel(); - _tokenSource = new CancellationTokenSource(); - FindDuplicates(_tokenSource.Token); + _duplicateService.Initialize(_entries); }); } @@ -294,12 +287,6 @@ public void OverwriteHosts() _ = Task.Run(SaveAsync); } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (Filtered && (e.PropertyName == nameof(Entry.Hosts) @@ -326,82 +313,6 @@ private void Entries_CollectionChanged(object sender, System.Collections.Special _ = Task.Run(SaveAsync); } - private void FindDuplicates(CancellationToken cancellationToken) - { - foreach (var entry in _entries) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - - if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address)) - { - continue; - } - - SetDuplicate(entry); - } - catch (OperationCanceledException) - { - LoggerInstance.Logger.LogInfo("FindDuplicates cancelled"); - return; - } - } - } - - private void FindDuplicates(string address, IEnumerable hosts) - { - var entries = _entries.Where(e => - string.Equals(e.Address, address, StringComparison.OrdinalIgnoreCase) - || hosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any()); - - foreach (var entry in entries) - { - SetDuplicate(entry); - } - } - - private void SetDuplicate(Entry entry) - { - if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address)) - { - _dispatcherQueue.TryEnqueue(() => - { - entry.Duplicate = false; - }); - - return; - } - - var duplicate = false; - - /* - * Duplicate are based on the following criteria: - * Entries with the same type and at least one host in common - * Entries with the same type and address, except when there is only one entry with less than 9 hosts for that type and address - */ - if (_entries.Any(e => e != entry - && e.Type == entry.Type - && entry.SplittedHosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any())) - { - duplicate = true; - } - else if (_entries.Any(e => e != entry - && e.Type == entry.Type - && string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase))) - { - duplicate = entry.SplittedHosts.Length < Consts.MaxHostsCount - && _entries.Count(e => e.Type == entry.Type - && string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase) - && e.SplittedHosts.Length < Consts.MaxHostsCount) > 1; - } - - _dispatcherQueue.TryEnqueue(() => - { - entry.Duplicate = duplicate; - }); - } - private async Task SaveAsync() { bool error = true; @@ -444,17 +355,5 @@ private async Task SaveAsync() IsReadOnly = isReadOnly; }); } - - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _hostsService?.Dispose(); - _disposed = true; - } - } - } } } From 68993bb2493474a46c4251aee8ac2e86c03e3bac Mon Sep 17 00:00:00 2001 From: Davide Giacometti Date: Fri, 10 May 2024 14:00:32 +0200 Subject: [PATCH 2/2] addressed feedback --- src/modules/Hosts/HostsUILib/Helpers/DuplicateService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/Hosts/HostsUILib/Helpers/DuplicateService.cs b/src/modules/Hosts/HostsUILib/Helpers/DuplicateService.cs index 51de3fd99fd..f6f89e6dbb0 100644 --- a/src/modules/Hosts/HostsUILib/Helpers/DuplicateService.cs +++ b/src/modules/Hosts/HostsUILib/Helpers/DuplicateService.cs @@ -26,6 +26,7 @@ private record struct Check(string Address, string[] Hosts); private readonly string[] _loopbackAddresses = { "0.0.0.0", + "::", "::0", "0:0:0:0:0:0:0:0", "127.0.0.1",