[Hosts]Duplicate check improvements (#32805)

* moved duplicate check in a dedicate service and made it async

* addressed feedback
This commit is contained in:
Davide Giacometti
2024-06-03 11:26:05 +02:00
committed by GitHub
parent e2f1ad6d40
commit c00f37c8d7
7 changed files with 200 additions and 121 deletions

View File

@@ -47,6 +47,7 @@ namespace Hosts
services.AddSingleton<IHostsService, HostsService>(); services.AddSingleton<IHostsService, HostsService>();
services.AddSingleton<IUserSettings, Hosts.Settings.UserSettings>(); services.AddSingleton<IUserSettings, Hosts.Settings.UserSettings>();
services.AddSingleton<IElevationHelper, ElevationHelper>(); services.AddSingleton<IElevationHelper, ElevationHelper>();
services.AddSingleton<IDuplicateService, DuplicateService>();
// Views and ViewModels // Views and ViewModels
services.AddSingleton<ILogger, LoggerWrapper>(); services.AddSingleton<ILogger, LoggerWrapper>();

View File

@@ -5,7 +5,6 @@
using System; using System;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Threading; using System.Threading;
using HostsUILib.Helpers;
using HostsUILib.Settings; using HostsUILib.Settings;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
@@ -45,6 +44,8 @@ namespace Hosts.Settings
// Moved from Settings.UI.Library // Moved from Settings.UI.Library
public HostsEncoding Encoding { get; set; } public HostsEncoding Encoding { get; set; }
public event EventHandler LoopbackDuplicatesChanged;
public UserSettings() public UserSettings()
{ {
_settingsUtils = new SettingsUtils(); _settingsUtils = new SettingsUtils();
@@ -58,8 +59,6 @@ namespace Hosts.Settings
_watcher = Helper.GetFileWatcher(HostsModuleName, "settings.json", () => LoadSettingsFromJson()); _watcher = Helper.GetFileWatcher(HostsModuleName, "settings.json", () => LoadSettingsFromJson());
} }
public event EventHandler LoopbackDuplicatesChanged;
private void LoadSettingsFromJson() private void LoadSettingsFromJson()
{ {
lock (_loadingSettingsLock) lock (_loadingSettingsLock)

View File

@@ -0,0 +1,165 @@
// 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<Check> _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<Entry> _entries;
private bool _disposed;
public DuplicateService(IUserSettings userSettings)
{
_userSettings = userSettings;
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_checkQueue = new Queue<Check>();
_checkEvent = new ManualResetEvent(false);
_queueThread = new Thread(ProcessQueue);
_queueThread.IsBackground = true;
_queueThread.Start();
}
public void Initialize(IList<Entry> 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;
}
}
}
}
}

View File

@@ -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<Entry> entries);
void CheckDuplicates(string address, string[] hosts);
}
}

View File

@@ -9,7 +9,7 @@ using HostsUILib.Models;
namespace HostsUILib.Helpers namespace HostsUILib.Helpers
{ {
public interface IHostsService : IDisposable public interface IHostsService
{ {
string HostsFilePath { get; } string HostsFilePath { get; }

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Net;
namespace HostsUILib.Settings namespace HostsUILib.Settings
{ {

View File

@@ -8,7 +8,6 @@ using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -23,21 +22,14 @@ using static HostsUILib.Settings.IUserSettings;
namespace HostsUILib.ViewModels namespace HostsUILib.ViewModels
{ {
public partial class MainViewModel : ObservableObject, IDisposable public partial class MainViewModel : ObservableObject
{ {
private readonly IHostsService _hostsService; private readonly IHostsService _hostsService;
private readonly IUserSettings _userSettings; private readonly IUserSettings _userSettings;
private readonly IDuplicateService _duplicateService;
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); 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 _readingHosts;
private bool _disposed;
private CancellationTokenSource _tokenSource;
[ObservableProperty] [ObservableProperty]
private Entry _selected; private Entry _selected;
@@ -95,10 +87,16 @@ namespace HostsUILib.ViewModels
private OpenSettingsFunction _openSettingsFunction; 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; _hostsService = hostService;
_userSettings = userSettings; _userSettings = userSettings;
_duplicateService = duplicateService;
_hostsService.FileChanged += (s, e) => _dispatcherQueue.TryEnqueue(() => FileChanged = true); _hostsService.FileChanged += (s, e) => _dispatcherQueue.TryEnqueue(() => FileChanged = true);
_userSettings.LoopbackDuplicatesChanged += (s, e) => ReadHosts(); _userSettings.LoopbackDuplicatesChanged += (s, e) => ReadHosts();
@@ -111,8 +109,7 @@ namespace HostsUILib.ViewModels
{ {
entry.PropertyChanged += Entry_PropertyChanged; entry.PropertyChanged += Entry_PropertyChanged;
_entries.Add(entry); _entries.Add(entry);
_duplicateService.CheckDuplicates(entry.Address, entry.SplittedHosts);
FindDuplicates(entry.Address, entry.SplittedHosts);
} }
public void Update(int index, Entry entry) public void Update(int index, Entry entry)
@@ -126,8 +123,8 @@ namespace HostsUILib.ViewModels
existingEntry.Hosts = entry.Hosts; existingEntry.Hosts = entry.Hosts;
existingEntry.Active = entry.Active; existingEntry.Active = entry.Active;
FindDuplicates(oldAddress, oldHosts); _duplicateService.CheckDuplicates(oldAddress, oldHosts);
FindDuplicates(entry.Address, entry.SplittedHosts); _duplicateService.CheckDuplicates(entry.Address, entry.SplittedHosts);
} }
public void DeleteSelected() public void DeleteSelected()
@@ -135,8 +132,7 @@ namespace HostsUILib.ViewModels
var address = Selected.Address; var address = Selected.Address;
var hosts = Selected.SplittedHosts; var hosts = Selected.SplittedHosts;
_entries.Remove(Selected); _entries.Remove(Selected);
_duplicateService.CheckDuplicates(address, hosts);
FindDuplicates(address, hosts);
} }
public void UpdateAdditionalLines(string lines) public void UpdateAdditionalLines(string lines)
@@ -169,8 +165,7 @@ namespace HostsUILib.ViewModels
var address = entry.Address; var address = entry.Address;
var hosts = entry.SplittedHosts; var hosts = entry.SplittedHosts;
_entries.Remove(entry); _entries.Remove(entry);
_duplicateService.CheckDuplicates(address, hosts);
FindDuplicates(address, hosts);
} }
} }
@@ -213,9 +208,7 @@ namespace HostsUILib.ViewModels
}); });
_readingHosts = false; _readingHosts = false;
_tokenSource?.Cancel(); _duplicateService.Initialize(_entries);
_tokenSource = new CancellationTokenSource();
FindDuplicates(_tokenSource.Token);
}); });
} }
@@ -294,12 +287,6 @@ namespace HostsUILib.ViewModels
_ = Task.Run(SaveAsync); _ = Task.Run(SaveAsync);
} }
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{ {
if (Filtered && (e.PropertyName == nameof(Entry.Hosts) if (Filtered && (e.PropertyName == nameof(Entry.Hosts)
@@ -326,82 +313,6 @@ namespace HostsUILib.ViewModels
_ = Task.Run(SaveAsync); _ = 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<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;
});
}
private async Task SaveAsync() private async Task SaveAsync()
{ {
bool error = true; bool error = true;
@@ -444,17 +355,5 @@ namespace HostsUILib.ViewModels
IsReadOnly = isReadOnly; IsReadOnly = isReadOnly;
}); });
} }
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_hostsService?.Dispose();
_disposed = true;
}
}
}
} }
} }