mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-08 12:18:50 +02:00
[Hosts]Handle read-only hosts file (#29562)
* handle read-only hosts file
This commit is contained in:
committed by
GitHub
parent
d5b9c31847
commit
16e26a200e
@@ -5,6 +5,7 @@
|
|||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Hosts.Exceptions;
|
||||||
using Hosts.Helpers;
|
using Hosts.Helpers;
|
||||||
using Hosts.Models;
|
using Hosts.Models;
|
||||||
using Hosts.Settings;
|
using Hosts.Settings;
|
||||||
@@ -18,11 +19,13 @@ namespace Hosts.Tests
|
|||||||
[TestClass]
|
[TestClass]
|
||||||
public class HostsServiceTest
|
public class HostsServiceTest
|
||||||
{
|
{
|
||||||
|
private static Mock<IUserSettings> _userSettings;
|
||||||
private static Mock<IElevationHelper> _elevationHelper;
|
private static Mock<IElevationHelper> _elevationHelper;
|
||||||
|
|
||||||
[ClassInitialize]
|
[ClassInitialize]
|
||||||
public static void ClassInitialize(TestContext context)
|
public static void ClassInitialize(TestContext context)
|
||||||
{
|
{
|
||||||
|
_userSettings = new Mock<IUserSettings>();
|
||||||
_elevationHelper = new Mock<IElevationHelper>();
|
_elevationHelper = new Mock<IElevationHelper>();
|
||||||
_elevationHelper.Setup(m => m.IsElevated).Returns(true);
|
_elevationHelper.Setup(m => m.IsElevated).Returns(true);
|
||||||
}
|
}
|
||||||
@@ -31,8 +34,7 @@ namespace Hosts.Tests
|
|||||||
public void Hosts_Exists()
|
public void Hosts_Exists()
|
||||||
{
|
{
|
||||||
var fileSystem = new CustomMockFileSystem();
|
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));
|
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
|
||||||
var result = service.Exists();
|
var result = service.Exists();
|
||||||
|
|
||||||
@@ -43,8 +45,7 @@ namespace Hosts.Tests
|
|||||||
public void Hosts_Not_Exists()
|
public void Hosts_Not_Exists()
|
||||||
{
|
{
|
||||||
var fileSystem = new CustomMockFileSystem();
|
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();
|
var result = service.Exists();
|
||||||
|
|
||||||
Assert.IsFalse(result);
|
Assert.IsFalse(result);
|
||||||
@@ -65,8 +66,7 @@ namespace Hosts.Tests
|
|||||||
";
|
";
|
||||||
|
|
||||||
var fileSystem = new CustomMockFileSystem();
|
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));
|
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||||
|
|
||||||
var data = await service.ReadAsync();
|
var data = await service.ReadAsync();
|
||||||
@@ -91,8 +91,7 @@ namespace Hosts.Tests
|
|||||||
";
|
";
|
||||||
|
|
||||||
var fileSystem = new CustomMockFileSystem();
|
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));
|
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||||
|
|
||||||
var data = await service.ReadAsync();
|
var data = await service.ReadAsync();
|
||||||
@@ -118,8 +117,7 @@ namespace Hosts.Tests
|
|||||||
";
|
";
|
||||||
|
|
||||||
var fileSystem = new CustomMockFileSystem();
|
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));
|
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||||
|
|
||||||
var data = await service.ReadAsync();
|
var data = await service.ReadAsync();
|
||||||
@@ -138,9 +136,7 @@ namespace Hosts.Tests
|
|||||||
public async Task Empty_Hosts()
|
public async Task Empty_Hosts()
|
||||||
{
|
{
|
||||||
var fileSystem = new CustomMockFileSystem();
|
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));
|
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
|
||||||
|
|
||||||
await service.WriteAsync(string.Empty, Enumerable.Empty<Entry>());
|
await service.WriteAsync(string.Empty, Enumerable.Empty<Entry>());
|
||||||
@@ -203,7 +199,6 @@ namespace Hosts.Tests
|
|||||||
var fileSystem = new CustomMockFileSystem();
|
var fileSystem = new CustomMockFileSystem();
|
||||||
var userSettings = new Mock<IUserSettings>();
|
var userSettings = new Mock<IUserSettings>();
|
||||||
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Bottom);
|
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Bottom);
|
||||||
|
|
||||||
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));
|
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||||
|
|
||||||
@@ -228,8 +223,7 @@ namespace Hosts.Tests
|
|||||||
";
|
";
|
||||||
|
|
||||||
var fileSystem = new CustomMockFileSystem();
|
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));
|
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||||
|
|
||||||
var data = await service.ReadAsync();
|
var data = await service.ReadAsync();
|
||||||
@@ -243,12 +237,37 @@ namespace Hosts.Tests
|
|||||||
public async Task Save_NotRunningElevatedException()
|
public async Task Save_NotRunningElevatedException()
|
||||||
{
|
{
|
||||||
var fileSystem = new CustomMockFileSystem();
|
var fileSystem = new CustomMockFileSystem();
|
||||||
var userSettings = new Mock<IUserSettings>();
|
|
||||||
var elevationHelper = new Mock<IElevationHelper>();
|
var elevationHelper = new Mock<IElevationHelper>();
|
||||||
elevationHelper.Setup(m => m.IsElevated).Returns(false);
|
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>()));
|
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;
|
using System;
|
||||||
|
|
||||||
namespace Hosts.Helpers
|
namespace Hosts.Exceptions
|
||||||
{
|
{
|
||||||
public class NotRunningElevatedException : Exception
|
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.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Hosts.Exceptions;
|
||||||
using Hosts.Models;
|
using Hosts.Models;
|
||||||
using Hosts.Settings;
|
using Hosts.Settings;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
@@ -129,6 +130,11 @@ namespace Hosts.Helpers
|
|||||||
throw new NotRunningElevatedException();
|
throw new NotRunningElevatedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_fileSystem.FileInfo.FromFileName(HostsFilePath).IsReadOnly)
|
||||||
|
{
|
||||||
|
throw new ReadOnlyHostsException();
|
||||||
|
}
|
||||||
|
|
||||||
var lines = new List<string>();
|
var lines = new List<string>();
|
||||||
|
|
||||||
if (entries.Any())
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(disposing: true);
|
Dispose(disposing: true);
|
||||||
|
|||||||
@@ -24,5 +24,7 @@ namespace Hosts.Helpers
|
|||||||
void CleanupBackup();
|
void CleanupBackup();
|
||||||
|
|
||||||
void OpenHostsFile();
|
void OpenHostsFile();
|
||||||
|
|
||||||
|
void RemoveReadOnly();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,7 +377,15 @@
|
|||||||
IsOpen="{x:Bind ViewModel.Error, Mode=TwoWay}"
|
IsOpen="{x:Bind ViewModel.Error, Mode=TwoWay}"
|
||||||
Message="{x:Bind ViewModel.ErrorMessage, Mode=TwoWay}"
|
Message="{x:Bind ViewModel.ErrorMessage, Mode=TwoWay}"
|
||||||
Severity="Error"
|
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
|
<InfoBar
|
||||||
x:Uid="FileChanged"
|
x:Uid="FileChanged"
|
||||||
Margin="0,8,0,0"
|
Margin="0,8,0,0"
|
||||||
@@ -385,7 +393,10 @@
|
|||||||
Severity="Informational"
|
Severity="Informational"
|
||||||
Visibility="{x:Bind ViewModel.FileChanged, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
Visibility="{x:Bind ViewModel.FileChanged, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
<InfoBar.ActionButton>
|
<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.ActionButton>
|
||||||
</InfoBar>
|
</InfoBar>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -228,6 +228,10 @@
|
|||||||
<value>The hosts file cannot be saved because the program isn't running as administrator.</value>
|
<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>
|
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||||
</data>
|
</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">
|
<data name="FilterBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||||
<value>Filters</value>
|
<value>Filters</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -246,6 +250,9 @@
|
|||||||
<value>Hosts</value>
|
<value>Hosts</value>
|
||||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="MakeWritable.Content" xml:space="preserve">
|
||||||
|
<value>Make writable</value>
|
||||||
|
</data>
|
||||||
<data name="MoveDown.Text" xml:space="preserve">
|
<data name="MoveDown.Text" xml:space="preserve">
|
||||||
<value>Move down</value>
|
<value>Move down</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CommunityToolkit.WinUI;
|
using CommunityToolkit.WinUI;
|
||||||
using CommunityToolkit.WinUI.Collections;
|
using CommunityToolkit.WinUI.Collections;
|
||||||
|
using Hosts.Exceptions;
|
||||||
using Hosts.Helpers;
|
using Hosts.Helpers;
|
||||||
using Hosts.Models;
|
using Hosts.Models;
|
||||||
using Hosts.Settings;
|
using Hosts.Settings;
|
||||||
@@ -48,6 +49,9 @@ namespace Hosts.ViewModels
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _errorMessage;
|
private string _errorMessage;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isReadOnly;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _fileChanged;
|
private bool _fileChanged;
|
||||||
|
|
||||||
@@ -262,6 +266,13 @@ namespace Hosts.ViewModels
|
|||||||
_hostsService.OpenHostsFile();
|
_hostsService.OpenHostsFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public void OverwriteHosts()
|
||||||
|
{
|
||||||
|
_hostsService.RemoveReadOnly();
|
||||||
|
_ = Task.Run(SaveAsync);
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(disposing: true);
|
Dispose(disposing: true);
|
||||||
@@ -374,6 +385,7 @@ namespace Hosts.ViewModels
|
|||||||
{
|
{
|
||||||
bool error = true;
|
bool error = true;
|
||||||
string errorMessage = string.Empty;
|
string errorMessage = string.Empty;
|
||||||
|
bool isReadOnly = false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -385,6 +397,12 @@ namespace Hosts.ViewModels
|
|||||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||||
errorMessage = resourceLoader.GetString("FileSaveError_NotElevated");
|
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)
|
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
|
// 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;
|
Error = error;
|
||||||
ErrorMessage = errorMessage;
|
ErrorMessage = errorMessage;
|
||||||
|
IsReadOnly = isReadOnly;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user