mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-01-22 08:53:08 +08:00
[Hosts]Handle read-only hosts file (#29562)
* handle read-only hosts file
This commit is contained in:
parent
d5b9c31847
commit
16e26a200e
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace Hosts.Helpers
|
||||
namespace Hosts.Exceptions
|
||||
{
|
||||
public class NotRunningElevatedException : Exception
|
||||
{
|
12
src/modules/Hosts/Hosts/Exceptions/ReadOnlyHostsException.cs
Normal file
12
src/modules/Hosts/Hosts/Exceptions/ReadOnlyHostsException.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -24,5 +24,7 @@ namespace Hosts.Helpers
|
||||
void CleanupBackup();
|
||||
|
||||
void OpenHostsFile();
|
||||
|
||||
void RemoveReadOnly();
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user