[EnvVar][Hosts][RegPrev]Decouple and refactor to make it "packable" as nuget package (#32604)

* WIP Hosts - remove deps

* Add consumer app

* Move App and MainWindow to Consumer app. Make Hosts dll

* Try consume it

* Fix errors

* Make it work with custom build targets

* Dependency injection
Refactor
Explicit page creation
Wire missing dependencies

* Fix installer

* Remove unneeded stuff

* Fix build again

* Extract UI and logic from MainWindow to RegistryPreviewMainPage

* Convert to lib
Change namespace to RegistryPreviewUILib
Remove PT deps

* Add exe app and move App.xaml and MainWindow.xaml

* Consume the lib

* Update Hosts package creation

* Fix RegistryPreview package creation

* Rename RegistryPreviewUI back to RegistryPreview

* Back to consuming lib

* Ship icons and assets in nuget packages

* Rename to EnvironmentVariablesUILib and convert to lib

* Add app and consume

* Telemetry

* GPO

* nuget

* Rename HostsPackageConsumer to Hosts and Hosts lib to HostsUILib

* Assets cleanup

* nuget struct

* v0

* assets

* [Hosts] Re-add AppList to Lib Assets, [RegPrev] Copy lib assets to out dir

* Sign UI dlls

* Revert WinUIEx bump

* Cleanup

* Align deps

* version exception dll

* Fix RegistryPreview crashes

* XAML format

* XAML format 2

* Pack .pri files in lib/ dir

---------

Co-authored-by: Darshak Bhatti <dabhatti@microsoft.com>
This commit is contained in:
Stefan Markovic
2024-04-26 19:41:44 +02:00
committed by GitHub
parent 28ba2bd301
commit 41a0114efe
125 changed files with 2097 additions and 1212 deletions

View File

@@ -0,0 +1,460 @@
// 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.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Collections;
using HostsUILib.Exceptions;
using HostsUILib.Helpers;
using HostsUILib.Models;
using HostsUILib.Settings;
using Microsoft.UI.Dispatching;
using static HostsUILib.Settings.IUserSettings;
namespace HostsUILib.ViewModels
{
public partial class MainViewModel : ObservableObject, IDisposable
{
private readonly IHostsService _hostsService;
private readonly IUserSettings _userSettings;
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;
[ObservableProperty]
private bool _error;
[ObservableProperty]
private string _errorMessage;
[ObservableProperty]
private bool _isReadOnly;
[ObservableProperty]
private bool _fileChanged;
[ObservableProperty]
private string _addressFilter;
[ObservableProperty]
private string _hostsFilter;
[ObservableProperty]
private string _commentFilter;
[ObservableProperty]
private string _additionalLines;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private bool _filtered;
[ObservableProperty]
private bool _showOnlyDuplicates;
[ObservableProperty]
private bool _showSplittedEntriesTooltip;
partial void OnShowOnlyDuplicatesChanged(bool value)
{
ApplyFilters();
}
private ObservableCollection<Entry> _entries;
public AdvancedCollectionView Entries { get; set; }
public int NextId => _entries?.Count > 0 ? _entries.Max(e => e.Id) + 1 : 0;
public IUserSettings UserSettings => _userSettings;
public static MainViewModel Instance { get; set; }
private OpenSettingsFunction _openSettingsFunction;
public MainViewModel(IHostsService hostService, IUserSettings userSettings, ILogger logger, OpenSettingsFunction openSettingsFunction)
{
_hostsService = hostService;
_userSettings = userSettings;
_hostsService.FileChanged += (s, e) => _dispatcherQueue.TryEnqueue(() => FileChanged = true);
_userSettings.LoopbackDuplicatesChanged += (s, e) => ReadHosts();
LoggerInstance.Logger = logger;
_openSettingsFunction = openSettingsFunction;
}
public void Add(Entry entry)
{
entry.PropertyChanged += Entry_PropertyChanged;
_entries.Add(entry);
FindDuplicates(entry.Address, entry.SplittedHosts);
}
public void Update(int index, Entry entry)
{
var existingEntry = Entries[index] as Entry;
var oldAddress = existingEntry.Address;
var oldHosts = existingEntry.SplittedHosts;
existingEntry.Address = entry.Address;
existingEntry.Comment = entry.Comment;
existingEntry.Hosts = entry.Hosts;
existingEntry.Active = entry.Active;
FindDuplicates(oldAddress, oldHosts);
FindDuplicates(entry.Address, entry.SplittedHosts);
}
public void DeleteSelected()
{
var address = Selected.Address;
var hosts = Selected.SplittedHosts;
_entries.Remove(Selected);
FindDuplicates(address, hosts);
}
public void UpdateAdditionalLines(string lines)
{
AdditionalLines = lines;
_ = Task.Run(SaveAsync);
}
public void Move(int oldIndex, int newIndex)
{
if (Filtered)
{
return;
}
// Swap the IDs
var entry1 = _entries[oldIndex];
var entry2 = _entries[newIndex];
(entry2.Id, entry1.Id) = (entry1.Id, entry2.Id);
// Move entries in the UI
_entries.Move(oldIndex, newIndex);
}
[RelayCommand]
public void DeleteEntry(Entry entry)
{
if (entry is not null)
{
var address = entry.Address;
var hosts = entry.SplittedHosts;
_entries.Remove(entry);
FindDuplicates(address, hosts);
}
}
[RelayCommand]
public void ReadHosts()
{
if (_readingHosts)
{
return;
}
_dispatcherQueue.TryEnqueue(() =>
{
FileChanged = false;
IsLoading = true;
});
Task.Run(async () =>
{
_readingHosts = true;
var data = await _hostsService.ReadAsync();
await _dispatcherQueue.EnqueueAsync(() =>
{
ShowSplittedEntriesTooltip = data.SplittedEntries;
AdditionalLines = data.AdditionalLines;
_entries = new ObservableCollection<Entry>(data.Entries);
foreach (var e in _entries)
{
e.PropertyChanged += Entry_PropertyChanged;
}
_entries.CollectionChanged += Entries_CollectionChanged;
Entries = new AdvancedCollectionView(_entries, true);
Entries.SortDescriptions.Add(new SortDescription(nameof(Entry.Id), SortDirection.Ascending));
ApplyFilters();
OnPropertyChanged(nameof(Entries));
IsLoading = false;
});
_readingHosts = false;
_tokenSource?.Cancel();
_tokenSource = new CancellationTokenSource();
FindDuplicates(_tokenSource.Token);
});
}
[RelayCommand]
public void ApplyFilters()
{
var expressions = new List<Expression<Func<object, bool>>>(4);
if (!string.IsNullOrWhiteSpace(AddressFilter))
{
expressions.Add(e => ((Entry)e).Address.Contains(AddressFilter, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(HostsFilter))
{
expressions.Add(e => ((Entry)e).Hosts.Contains(HostsFilter, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(CommentFilter))
{
expressions.Add(e => ((Entry)e).Comment.Contains(CommentFilter, StringComparison.OrdinalIgnoreCase));
}
if (ShowOnlyDuplicates)
{
expressions.Add(e => ((Entry)e).Duplicate);
}
Expression<Func<object, bool>> filterExpression = null;
foreach (var e in expressions)
{
filterExpression = filterExpression == null ? e : filterExpression.And(e);
}
Filtered = filterExpression != null;
Entries.Filter = Filtered ? filterExpression.Compile().Invoke : null;
Entries.RefreshFilter();
}
[RelayCommand]
public void ClearFilters()
{
AddressFilter = null;
HostsFilter = null;
CommentFilter = null;
ShowOnlyDuplicates = false;
ApplyFilters();
}
public async Task PingSelectedAsync()
{
var selected = Selected;
selected.Ping = null;
selected.Pinging = true;
selected.Ping = await _hostsService.PingAsync(Selected.Address);
selected.Pinging = false;
}
[RelayCommand]
public void OpenSettings()
{
_openSettingsFunction();
}
[RelayCommand]
public void OpenHostsFile()
{
_hostsService.OpenHostsFile();
}
[RelayCommand]
public void OverwriteHosts()
{
_hostsService.RemoveReadOnly();
_ = 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)
|| e.PropertyName == nameof(Entry.Address)
|| e.PropertyName == nameof(Entry.Comment)
|| e.PropertyName == nameof(Entry.Duplicate)))
{
Entries.RefreshFilter();
}
// Ping and duplicate should not trigger a file save
if (e.PropertyName == nameof(Entry.Ping)
|| e.PropertyName == nameof(Entry.Pinging)
|| e.PropertyName == nameof(Entry.Duplicate))
{
return;
}
_ = Task.Run(SaveAsync);
}
private void Entries_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
_ = 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()
{
bool error = true;
string errorMessage = string.Empty;
bool isReadOnly = false;
try
{
await _hostsService.WriteAsync(AdditionalLines, _entries);
error = false;
}
catch (NotRunningElevatedException)
{
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
errorMessage = resourceLoader.GetString("FileSaveError_NotElevated");
}
catch (ReadOnlyHostsException)
{
isReadOnly = true;
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
errorMessage = resourceLoader.GetString("FileSaveError_ReadOnly");
}
catch (IOException ex) when ((ex.HResult & 0x0000FFFF) == 32)
{
// There are some edge cases where a big hosts file is being locked by svchost.exe https://github.com/microsoft/PowerToys/issues/28066
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
errorMessage = resourceLoader.GetString("FileSaveError_FileInUse");
}
catch (Exception ex)
{
LoggerInstance.Logger.LogError("Failed to save hosts file", ex);
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
errorMessage = resourceLoader.GetString("FileSaveError_Generic");
}
await _dispatcherQueue.EnqueueAsync(() =>
{
Error = error;
ErrorMessage = errorMessage;
IsReadOnly = isReadOnly;
});
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_hostsService?.Dispose();
_disposed = true;
}
}
}
}
}