[Hosts]Handle read-only hosts file (#29562)

* handle read-only hosts file
This commit is contained in:
Davide Giacometti 2023-11-03 17:10:26 +01:00 committed by GitHub
parent d5b9c31847
commit 16e26a200e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 106 additions and 21 deletions

View File

@ -5,6 +5,7 @@
using System.IO.Abstractions.TestingHelpers;
using System.Linq;
using System.Threading.Tasks;
using Hosts.Exceptions;
using Hosts.Helpers;
using Hosts.Models;
using Hosts.Settings;
@ -18,11 +19,13 @@ namespace Hosts.Tests
[TestClass]
public class HostsServiceTest
{
private static Mock<IUserSettings> _userSettings;
private static Mock<IElevationHelper> _elevationHelper;
[ClassInitialize]
public static void ClassInitialize(TestContext context)
{
_userSettings = new Mock<IUserSettings>();
_elevationHelper = new Mock<IElevationHelper>();
_elevationHelper.Setup(m => m.IsElevated).Returns(true);
}
@ -31,8 +34,7 @@ namespace Hosts.Tests
public void Hosts_Exists()
{
var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
var result = service.Exists();
@ -43,8 +45,7 @@ namespace Hosts.Tests
public void Hosts_Not_Exists()
{
var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var result = service.Exists();
Assert.IsFalse(result);
@ -65,8 +66,7 @@ namespace Hosts.Tests
";
var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync();
@ -91,8 +91,7 @@ namespace Hosts.Tests
";
var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync();
@ -118,8 +117,7 @@ namespace Hosts.Tests
";
var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync();
@ -138,9 +136,7 @@ namespace Hosts.Tests
public async Task Empty_Hosts()
{
var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
await service.WriteAsync(string.Empty, Enumerable.Empty<Entry>());
@ -203,7 +199,6 @@ namespace Hosts.Tests
var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>();
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Bottom);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
@ -228,8 +223,7 @@ namespace Hosts.Tests
";
var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>();
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync();
@ -243,12 +237,37 @@ namespace Hosts.Tests
public async Task Save_NotRunningElevatedException()
{
var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>();
var elevationHelper = new Mock<IElevationHelper>();
elevationHelper.Setup(m => m.IsElevated).Returns(false);
var service = new HostsService(fileSystem, userSettings.Object, elevationHelper.Object);
var service = new HostsService(fileSystem, _userSettings.Object, elevationHelper.Object);
await Assert.ThrowsExceptionAsync<NotRunningElevatedException>(async () => await service.WriteAsync("# Empty hosts file", Enumerable.Empty<Entry>()));
}
[TestMethod]
public async Task Save_ReadOnlyHostsException()
{
var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var hostsFile = new MockFileData(string.Empty);
hostsFile.Attributes = System.IO.FileAttributes.ReadOnly;
fileSystem.AddFile(service.HostsFilePath, hostsFile);
await Assert.ThrowsExceptionAsync<ReadOnlyHostsException>(async () => await service.WriteAsync("# Empty hosts file", Enumerable.Empty<Entry>()));
}
[TestMethod]
public void Remove_ReadOnly()
{
var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var hostsFile = new MockFileData(string.Empty);
hostsFile.Attributes = System.IO.FileAttributes.ReadOnly;
fileSystem.AddFile(service.HostsFilePath, hostsFile);
service.RemoveReadOnly();
var readOnly = fileSystem.FileInfo.FromFileName(service.HostsFilePath).Attributes.HasFlag(System.IO.FileAttributes.ReadOnly);
Assert.IsFalse(readOnly);
}
}
}

View File

@ -4,7 +4,7 @@
using System;
namespace Hosts.Helpers
namespace Hosts.Exceptions
{
public class NotRunningElevatedException : Exception
{

View File

@ -0,0 +1,12 @@
// 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;
namespace Hosts.Exceptions
{
public class ReadOnlyHostsException : Exception
{
}
}

View File

@ -13,6 +13,7 @@ using System.Net.NetworkInformation;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Hosts.Exceptions;
using Hosts.Models;
using Hosts.Settings;
using ManagedCommon;
@ -129,6 +130,11 @@ namespace Hosts.Helpers
throw new NotRunningElevatedException();
}
if (_fileSystem.FileInfo.FromFileName(HostsFilePath).IsReadOnly)
{
throw new ReadOnlyHostsException();
}
var lines = new List<string>();
if (entries.Any())
@ -288,6 +294,15 @@ namespace Hosts.Helpers
}
}
public void RemoveReadOnly()
{
var fileInfo = _fileSystem.FileInfo.FromFileName(HostsFilePath);
if (fileInfo.IsReadOnly)
{
fileInfo.IsReadOnly = false;
}
}
public void Dispose()
{
Dispose(disposing: true);

View File

@ -24,5 +24,7 @@ namespace Hosts.Helpers
void CleanupBackup();
void OpenHostsFile();
void RemoveReadOnly();
}
}

View File

@ -377,7 +377,15 @@
IsOpen="{x:Bind ViewModel.Error, Mode=TwoWay}"
Message="{x:Bind ViewModel.ErrorMessage, Mode=TwoWay}"
Severity="Error"
Visibility="{x:Bind ViewModel.Error, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}" />
Visibility="{x:Bind ViewModel.Error, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}">
<InfoBar.ActionButton>
<Button
x:Uid="MakeWritable"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.OverwriteHostsCommand}"
Visibility="{x:Bind ViewModel.IsReadOnly, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</InfoBar.ActionButton>
</InfoBar>
<InfoBar
x:Uid="FileChanged"
Margin="0,8,0,0"
@ -385,7 +393,10 @@
Severity="Informational"
Visibility="{x:Bind ViewModel.FileChanged, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}">
<InfoBar.ActionButton>
<Button x:Uid="Reload" Command="{x:Bind ViewModel.ReadHostsCommand}" />
<Button
x:Uid="Reload"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.ReadHostsCommand}" />
</InfoBar.ActionButton>
</InfoBar>
</StackPanel>

View File

@ -228,6 +228,10 @@
<value>The hosts file cannot be saved because the program isn't running as administrator.</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="FileSaveError_ReadOnly" xml:space="preserve">
<value>The hosts file cannot be saved because it is read-only.</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="FilterBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Filters</value>
</data>
@ -246,6 +250,9 @@
<value>Hosts</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="MakeWritable.Content" xml:space="preserve">
<value>Make writable</value>
</data>
<data name="MoveDown.Text" xml:space="preserve">
<value>Move down</value>
</data>

View File

@ -15,6 +15,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Collections;
using Hosts.Exceptions;
using Hosts.Helpers;
using Hosts.Models;
using Hosts.Settings;
@ -48,6 +49,9 @@ namespace Hosts.ViewModels
[ObservableProperty]
private string _errorMessage;
[ObservableProperty]
private bool _isReadOnly;
[ObservableProperty]
private bool _fileChanged;
@ -262,6 +266,13 @@ namespace Hosts.ViewModels
_hostsService.OpenHostsFile();
}
[RelayCommand]
public void OverwriteHosts()
{
_hostsService.RemoveReadOnly();
_ = Task.Run(SaveAsync);
}
public void Dispose()
{
Dispose(disposing: true);
@ -374,6 +385,7 @@ namespace Hosts.ViewModels
{
bool error = true;
string errorMessage = string.Empty;
bool isReadOnly = false;
try
{
@ -385,6 +397,12 @@ namespace Hosts.ViewModels
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
@ -402,6 +420,7 @@ namespace Hosts.ViewModels
{
Error = error;
ErrorMessage = errorMessage;
IsReadOnly = isReadOnly;
});
}