[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,329 @@
// 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.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using HostsUILib.Exceptions;
using HostsUILib.Models;
using HostsUILib.Settings;
using Microsoft.Win32;
namespace HostsUILib.Helpers
{
public class HostsService : IHostsService, IDisposable
{
private const string _backupSuffix = $"_PowerToysBackup_";
private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
private readonly IFileSystem _fileSystem;
private readonly IUserSettings _userSettings;
private readonly IElevationHelper _elevationHelper;
private readonly IFileSystemWatcher _fileSystemWatcher;
private readonly string _hostsFilePath;
private bool _backupDone;
private bool _disposed;
public string HostsFilePath => _hostsFilePath;
public event EventHandler FileChanged;
public Encoding Encoding => _userSettings.Encoding == HostsEncoding.Utf8 ? new UTF8Encoding(false) : new UTF8Encoding(true);
public HostsService(
IFileSystem fileSystem,
IUserSettings userSettings,
IElevationHelper elevationHelper)
{
_fileSystem = fileSystem;
_userSettings = userSettings;
_elevationHelper = elevationHelper;
_hostsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"System32\drivers\etc\hosts");
_fileSystemWatcher = _fileSystem.FileSystemWatcher.CreateNew();
_fileSystemWatcher.Path = _fileSystem.Path.GetDirectoryName(HostsFilePath);
_fileSystemWatcher.Filter = _fileSystem.Path.GetFileName(HostsFilePath);
_fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite;
_fileSystemWatcher.Changed += FileSystemWatcher_Changed;
_fileSystemWatcher.EnableRaisingEvents = true;
}
public bool Exists()
{
return _fileSystem.File.Exists(HostsFilePath);
}
public async Task<HostsData> ReadAsync()
{
var entries = new List<Entry>();
var unparsedBuilder = new StringBuilder();
var splittedEntries = false;
if (!Exists())
{
return new HostsData(entries, unparsedBuilder.ToString(), false);
}
var lines = await _fileSystem.File.ReadAllLinesAsync(HostsFilePath, Encoding);
var id = 0;
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i];
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
var entry = new Entry(id, line);
if (entry.Valid)
{
entries.Add(entry);
id++;
}
else if (entry.Validate(false))
{
foreach (var hostsChunk in entry.SplittedHosts.Chunk(Consts.MaxHostsCount))
{
var clonedEntry = entry.Clone();
clonedEntry.Id = id;
clonedEntry.Hosts = string.Join(' ', hostsChunk);
entries.Add(clonedEntry);
id++;
}
splittedEntries = true;
}
else
{
if (unparsedBuilder.Length > 0)
{
unparsedBuilder.Append(Environment.NewLine);
}
unparsedBuilder.Append(line);
}
}
return new HostsData(entries, unparsedBuilder.ToString(), splittedEntries);
}
public async Task WriteAsync(string additionalLines, IEnumerable<Entry> entries)
{
if (!_elevationHelper.IsElevated)
{
throw new NotRunningElevatedException();
}
if (_fileSystem.FileInfo.FromFileName(HostsFilePath).IsReadOnly)
{
throw new ReadOnlyHostsException();
}
var lines = new List<string>();
if (entries.Any())
{
var addressPadding = entries.Max(e => e.Address.Length) + 1;
var hostsPadding = entries.Max(e => e.Hosts.Length) + 1;
var anyDisabled = entries.Any(e => !e.Active);
foreach (var e in entries)
{
var lineBuilder = new StringBuilder();
if (!e.Valid)
{
lineBuilder.Append(e.Line);
}
else
{
if (!e.Active)
{
lineBuilder.Append('#').Append(' ');
}
else if (anyDisabled)
{
lineBuilder.Append(' ').Append(' ');
}
lineBuilder.Append(e.Address.PadRight(addressPadding));
lineBuilder.Append(string.Join(' ', e.Hosts).PadRight(hostsPadding));
if (e.Comment != string.Empty)
{
lineBuilder.Append('#').Append(' ');
lineBuilder.Append(e.Comment);
}
lines.Add(lineBuilder.ToString().TrimEnd());
}
}
}
if (!string.IsNullOrWhiteSpace(additionalLines))
{
if (_userSettings.AdditionalLinesPosition == HostsAdditionalLinesPosition.Top)
{
lines.Insert(0, additionalLines);
}
else if (_userSettings.AdditionalLinesPosition == HostsAdditionalLinesPosition.Bottom)
{
lines.Add(additionalLines);
}
}
try
{
await _asyncLock.WaitAsync();
_fileSystemWatcher.EnableRaisingEvents = false;
if (!_backupDone && Exists())
{
_fileSystem.File.Copy(HostsFilePath, HostsFilePath + _backupSuffix + DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture));
_backupDone = true;
}
await _fileSystem.File.WriteAllLinesAsync(HostsFilePath, lines, Encoding);
}
finally
{
_fileSystemWatcher.EnableRaisingEvents = true;
_asyncLock.Release();
}
}
public async Task<bool> PingAsync(string address)
{
try
{
using var ping = new Ping();
var reply = await ping.SendPingAsync(address, 4000); // 4000 is the default ping timeout for ping.exe
return reply.Status == IPStatus.Success;
}
catch
{
return false;
}
}
public void CleanupBackup()
{
Directory.GetFiles(Path.GetDirectoryName(HostsFilePath), $"*{_backupSuffix}*")
.Select(f => new FileInfo(f))
.Where(f => f.CreationTime < DateTime.Now.AddDays(-15))
.ToList()
.ForEach(f => f.Delete());
}
public void OpenHostsFile()
{
var notepadFallback = false;
try
{
// Try to open in default editor
var key = Registry.ClassesRoot.OpenSubKey("SystemFileAssociations\\text\\shell\\edit\\command");
if (key != null)
{
var commandPattern = key.GetValue(string.Empty).ToString(); // Default value
var file = null as string;
var args = null as string;
if (commandPattern.StartsWith('\"'))
{
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
if (endQuoteIndex != -1)
{
file = commandPattern[1..endQuoteIndex];
args = commandPattern[(endQuoteIndex + 1)..].Trim();
}
}
else
{
var spaceIndex = commandPattern.IndexOf(' ');
if (spaceIndex != -1)
{
file = commandPattern[..spaceIndex];
args = commandPattern[(spaceIndex + 1)..].Trim();
}
}
if (file != null && args != null)
{
args = args.Replace("%1", HostsFilePath);
Process.Start(new ProcessStartInfo(file, args));
}
else
{
notepadFallback = true;
}
}
}
catch (Exception ex)
{
LoggerInstance.Logger.LogError("Failed to open default editor", ex);
notepadFallback = true;
}
if (notepadFallback)
{
try
{
Process.Start(new ProcessStartInfo("notepad.exe", HostsFilePath));
}
catch (Exception ex)
{
LoggerInstance.Logger.LogError("Failed to open notepad", ex);
}
}
}
public void RemoveReadOnly()
{
var fileInfo = _fileSystem.FileInfo.FromFileName(HostsFilePath);
if (fileInfo.IsReadOnly)
{
fileInfo.IsReadOnly = false;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
{
_fileSystemWatcher.EnableRaisingEvents = false;
FileChanged?.Invoke(this, EventArgs.Empty);
_fileSystemWatcher.EnableRaisingEvents = true;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_asyncLock.Dispose();
_disposed = true;
}
}
}
}
}