mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-04 02:06:36 +02:00
Hosts file editor (#20462)
This commit is contained in:
committed by
GitHub
parent
ab41b61e84
commit
b2e1337d4e
83
src/modules/Hosts/Hosts.Tests/EntryTest.cs
Normal file
83
src/modules/Hosts/Hosts.Tests/EntryTest.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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 Hosts.Models;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Hosts.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class EntryTest
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow("\t\t10.1.1.1\t\thost\t\t", "10.1.1.1", "host", "", true)]
|
||||
[DataRow(" 10.1.1.1 host ", "10.1.1.1", "host", "", true)]
|
||||
[DataRow("10.1.1.1 host", "10.1.1.1", "host", "", true)]
|
||||
[DataRow("\t\t#\t\t10.1.1.1\thost\t\t", "10.1.1.1", "host", "", false)]
|
||||
[DataRow(" # 10.1.1.1 host ", "10.1.1.1", "host", "", false)]
|
||||
[DataRow("#10.1.1.1 host", "10.1.1.1", "host", "", false)]
|
||||
[DataRow("\t\t10.1.1.1\t\thost\t\t#\t\tcomment\t\t", "10.1.1.1", "host", "comment", true)]
|
||||
[DataRow(" 10.1.1.1 host # comment ", "10.1.1.1", "host", "comment", true)]
|
||||
[DataRow("10.1.1.1 host#comment", "10.1.1.1", "host", "comment", true)]
|
||||
[DataRow("\t\t#\t\t10.1.1.1\thost\t\t#\t\tcomment\t\t", "10.1.1.1", "host", "comment", false)]
|
||||
[DataRow(" # 10.1.1.1 host # comment ", "10.1.1.1", "host", "comment", false)]
|
||||
[DataRow("#10.1.1.1 host#comment", "10.1.1.1", "host", "comment", false)]
|
||||
[DataRow("# #10.1.1.1 host#comment", "10.1.1.1", "host", "comment", false)]
|
||||
[DataRow("# #\t10.1.1.1 host#comment", "10.1.1.1", "host", "comment", false)]
|
||||
[DataRow("# # \t10.1.1.1 host#comment", "10.1.1.1", "host", "comment", false)]
|
||||
public void Valid_Entry_SingleHost(string line, string address, string host, string comment, bool active)
|
||||
{
|
||||
var entry = new Entry(line);
|
||||
|
||||
Assert.AreEqual(entry.Address, address);
|
||||
Assert.AreEqual(entry.Hosts, host);
|
||||
Assert.AreEqual(entry.Comment, comment);
|
||||
Assert.AreEqual(entry.Active, active);
|
||||
Assert.IsTrue(entry.Valid);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("\t\t10.1.1.1\t\thost host.local\t\t", "10.1.1.1", "host host.local", "", true)]
|
||||
[DataRow(" 10.1.1.1 host host.local ", "10.1.1.1", "host host.local", "", true)]
|
||||
[DataRow("10.1.1.1 host host.local", "10.1.1.1", "host host.local", "", true)]
|
||||
[DataRow("\t\t#\t\t10.1.1.1\thost\t\thost.local\t\t", "10.1.1.1", "host host.local", "", false)]
|
||||
[DataRow(" # 10.1.1.1 host host.local ", "10.1.1.1", "host host.local", "", false)]
|
||||
[DataRow("#10.1.1.1 host host.local", "10.1.1.1", "host host.local", "", false)]
|
||||
[DataRow("\t\t10.1.1.1\t\thost\t\thost.local\t\t#\t\tcomment\t\t", "10.1.1.1", "host host.local", "comment", true)]
|
||||
[DataRow(" 10.1.1.1 host host.local # comment ", "10.1.1.1", "host host.local", "comment", true)]
|
||||
[DataRow("10.1.1.1 host host.local#comment", "10.1.1.1", "host host.local", "comment", true)]
|
||||
[DataRow("\t\t#\t\t10.1.1.1\thost\t\thost.local\t\t#\t\tcomment\t\t", "10.1.1.1", "host host.local", "comment", false)]
|
||||
[DataRow(" # 10.1.1.1 host host.local # comment ", "10.1.1.1", "host host.local", "comment", false)]
|
||||
[DataRow("#10.1.1.1 host host.local#comment", "10.1.1.1", "host host.local", "comment", false)]
|
||||
public void Valid_Entry_MultipleHosts(string line, string address, string host, string comment, bool active)
|
||||
{
|
||||
var entry = new Entry(line);
|
||||
|
||||
Assert.AreEqual(entry.Address, address);
|
||||
Assert.AreEqual(entry.Hosts, host);
|
||||
Assert.AreEqual(entry.Comment, comment);
|
||||
Assert.AreEqual(entry.Active, active);
|
||||
Assert.IsTrue(entry.Valid);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("\t\t10.1.1.1\t\t")]
|
||||
[DataRow(" 10.1.1.1 ")]
|
||||
[DataRow("10.1.1.1")]
|
||||
[DataRow("\t\thost\t\t")]
|
||||
[DataRow(" host ")]
|
||||
[DataRow("host")]
|
||||
[DataRow("\t\t10\t\thost")]
|
||||
[DataRow(" 10 host ")]
|
||||
[DataRow("10 host")]
|
||||
[DataRow("\t\thost\t\t10.1.1.1")]
|
||||
[DataRow(" host 10.1.1.1")]
|
||||
[DataRow("host 10.1.1.1")]
|
||||
public void Not_Valid_Entry(string line)
|
||||
{
|
||||
var entry = new Entry(line);
|
||||
Assert.IsFalse(entry.Valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/modules/Hosts/Hosts.Tests/Hosts.Tests.csproj
Normal file
30
src/modules/Hosts/Hosts.Tests/Hosts.Tests.csproj
Normal file
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\Version.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
|
||||
<RuntimeIdentifiers>win10-x64;win10-arm64</RuntimeIdentifiers>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\modules\Hosts\Hosts.Tests\</OutputPath>
|
||||
<IntermediateOutputPath>$(SolutionDir)$(Platform)\$(Configuration)\obj\$(AssemblyName)\</IntermediateOutputPath>
|
||||
<RootNamespace>Hosts.Tests</RootNamespace>
|
||||
<AssemblyName>PowerToys.Hosts.Tests</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.3" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.3" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="12.2.5" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="12.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Hosts\Hosts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
255
src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs
Normal file
255
src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Hosts.Helpers;
|
||||
using Hosts.Models;
|
||||
using Hosts.Settings;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
using Settings.UI.Library.Enumerations;
|
||||
|
||||
namespace Hosts.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class HostsServiceTest
|
||||
{
|
||||
private static Mock<IElevationHelper> _elevationHelper;
|
||||
|
||||
[ClassInitialize]
|
||||
public static void ClassInitialize(TestContext context)
|
||||
{
|
||||
_elevationHelper = new Mock<IElevationHelper>();
|
||||
_elevationHelper.Setup(m => m.IsElevated).Returns(true);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Hosts_Exists()
|
||||
{
|
||||
var fileSystem = new MockFileSystem
|
||||
{
|
||||
FileSystemWatcher = new TestFileSystemWatcherFactory(),
|
||||
};
|
||||
|
||||
var userSettings = new Mock<IUserSettings>();
|
||||
|
||||
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
|
||||
var result = service.Exists();
|
||||
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Hosts_Not_Exists()
|
||||
{
|
||||
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
||||
{
|
||||
})
|
||||
{
|
||||
FileSystemWatcher = new TestFileSystemWatcherFactory(),
|
||||
};
|
||||
|
||||
var userSettings = new Mock<IUserSettings>();
|
||||
|
||||
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
|
||||
var result = service.Exists();
|
||||
|
||||
Assert.IsFalse(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Host_Added()
|
||||
{
|
||||
var content =
|
||||
@"10.1.1.1 host host.local # comment
|
||||
10.1.1.2 host2 host2.local # another comment
|
||||
";
|
||||
|
||||
var contentResult =
|
||||
@" 10.1.1.1 host host.local # comment
|
||||
10.1.1.2 host2 host2.local # another comment
|
||||
# 10.1.1.30 host30 host30.local # new entry
|
||||
";
|
||||
|
||||
var fileSystem = new MockFileSystem
|
||||
{
|
||||
FileSystemWatcher = new TestFileSystemWatcherFactory(),
|
||||
};
|
||||
|
||||
var userSettings = new Mock<IUserSettings>();
|
||||
|
||||
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||
|
||||
var (_, entries) = await service.ReadAsync();
|
||||
entries.Add(new Entry("10.1.1.30", "host30 host30.local", "new entry", false));
|
||||
await service.WriteAsync(string.Empty, entries);
|
||||
|
||||
var result = fileSystem.GetFile(service.HostsFilePath);
|
||||
Assert.AreEqual(result.TextContents, contentResult);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Host_Deleted()
|
||||
{
|
||||
var content =
|
||||
@"10.1.1.1 host host.local # comment
|
||||
10.1.1.2 host2 host2.local # another comment
|
||||
";
|
||||
|
||||
var contentResult =
|
||||
@"10.1.1.2 host2 host2.local # another comment
|
||||
";
|
||||
|
||||
var fileSystem = new MockFileSystem
|
||||
{
|
||||
FileSystemWatcher = new TestFileSystemWatcherFactory(),
|
||||
};
|
||||
|
||||
var userSettings = new Mock<IUserSettings>();
|
||||
|
||||
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||
|
||||
var (_, entries) = await service.ReadAsync();
|
||||
entries.RemoveAt(0);
|
||||
await service.WriteAsync(string.Empty, entries);
|
||||
|
||||
var result = fileSystem.GetFile(service.HostsFilePath);
|
||||
Assert.AreEqual(result.TextContents, contentResult);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Host_Updated()
|
||||
{
|
||||
var content =
|
||||
@"10.1.1.1 host host.local # comment
|
||||
10.1.1.2 host2 host2.local # another comment
|
||||
";
|
||||
|
||||
var contentResult =
|
||||
@"# 10.1.1.10 host host.local host1.local # updated comment
|
||||
10.1.1.2 host2 host2.local # another comment
|
||||
";
|
||||
|
||||
var fileSystem = new MockFileSystem
|
||||
{
|
||||
FileSystemWatcher = new TestFileSystemWatcherFactory(),
|
||||
};
|
||||
|
||||
var userSettings = new Mock<IUserSettings>();
|
||||
|
||||
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||
|
||||
var (_, entries) = await service.ReadAsync();
|
||||
var entry = entries[0];
|
||||
entry.Address = "10.1.1.10";
|
||||
entry.Hosts = "host host.local host1.local";
|
||||
entry.Comment = "updated comment";
|
||||
entry.Active = false;
|
||||
await service.WriteAsync(string.Empty, entries);
|
||||
|
||||
var result = fileSystem.GetFile(service.HostsFilePath);
|
||||
Assert.AreEqual(result.TextContents, contentResult);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Empty_Hosts()
|
||||
{
|
||||
var fileSystem = new MockFileSystem
|
||||
{
|
||||
FileSystemWatcher = new TestFileSystemWatcherFactory(),
|
||||
};
|
||||
|
||||
var userSettings = new Mock<IUserSettings>();
|
||||
|
||||
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>());
|
||||
|
||||
var result = fileSystem.GetFile(service.HostsFilePath);
|
||||
Assert.AreEqual(result.TextContents, string.Empty);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AdditionalLines_Top()
|
||||
{
|
||||
var content =
|
||||
@"# header
|
||||
10.1.1.1 host host.local # comment
|
||||
# comment
|
||||
10.1.1.2 host2 host2.local # another comment
|
||||
# footer
|
||||
";
|
||||
|
||||
var contentResult =
|
||||
@"# header
|
||||
# comment
|
||||
# footer
|
||||
10.1.1.1 host host.local # comment
|
||||
10.1.1.2 host2 host2.local # another comment
|
||||
";
|
||||
|
||||
var fileSystem = new MockFileSystem
|
||||
{
|
||||
FileSystemWatcher = new TestFileSystemWatcherFactory(),
|
||||
};
|
||||
|
||||
var userSettings = new Mock<IUserSettings>();
|
||||
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(AdditionalLinesPosition.Top);
|
||||
|
||||
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||
|
||||
var (additionalLines, entries) = await service.ReadAsync();
|
||||
await service.WriteAsync(additionalLines, entries);
|
||||
|
||||
var result = fileSystem.GetFile(service.HostsFilePath);
|
||||
Assert.AreEqual(result.TextContents, contentResult);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AdditionalLines_Bottom()
|
||||
{
|
||||
var content =
|
||||
@"# header
|
||||
10.1.1.1 host host.local # comment
|
||||
# comment
|
||||
10.1.1.2 host2 host2.local # another comment
|
||||
# footer
|
||||
";
|
||||
|
||||
var contentResult =
|
||||
@"10.1.1.1 host host.local # comment
|
||||
10.1.1.2 host2 host2.local # another comment
|
||||
# header
|
||||
# comment
|
||||
# footer
|
||||
";
|
||||
|
||||
var fileSystem = new MockFileSystem
|
||||
{
|
||||
FileSystemWatcher = new TestFileSystemWatcherFactory(),
|
||||
};
|
||||
|
||||
var userSettings = new Mock<IUserSettings>();
|
||||
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(AdditionalLinesPosition.Bottom);
|
||||
|
||||
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||
|
||||
var (additionalLines, entries) = await service.ReadAsync();
|
||||
await service.WriteAsync(additionalLines, entries);
|
||||
|
||||
var result = fileSystem.GetFile(service.HostsFilePath);
|
||||
Assert.AreEqual(result.TextContents, contentResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/modules/Hosts/Hosts.Tests/TestFileSystemWatcher.cs
Normal file
49
src/modules/Hosts/Hosts.Tests/TestFileSystemWatcher.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
// 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.ComponentModel;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
|
||||
namespace Hosts.Tests
|
||||
{
|
||||
public class TestFileSystemWatcher : FileSystemWatcherBase
|
||||
{
|
||||
public override bool IncludeSubdirectories { get; set; }
|
||||
|
||||
public override bool EnableRaisingEvents { get; set; }
|
||||
|
||||
public override string Filter { get; set; }
|
||||
|
||||
public override int InternalBufferSize { get; set; }
|
||||
|
||||
public override NotifyFilters NotifyFilter { get; set; }
|
||||
|
||||
public override string Path { get; set; }
|
||||
|
||||
public override ISite Site { get; set; }
|
||||
|
||||
public override ISynchronizeInvoke SynchronizingObject { get; set; }
|
||||
|
||||
public override WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType) => default(WaitForChangedResult);
|
||||
|
||||
public override WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType, int timeout) => default(WaitForChangedResult);
|
||||
|
||||
public TestFileSystemWatcher(string path) => Path = path;
|
||||
|
||||
public TestFileSystemWatcher(string path, string filter)
|
||||
{
|
||||
Path = path;
|
||||
Filter = filter;
|
||||
}
|
||||
|
||||
public override void BeginInit()
|
||||
{
|
||||
}
|
||||
|
||||
public override void EndInit()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.IO.Abstractions;
|
||||
|
||||
namespace Hosts.Tests
|
||||
{
|
||||
public class TestFileSystemWatcherFactory : IFileSystemWatcherFactory
|
||||
{
|
||||
public IFileSystemWatcher CreateNew() => new TestFileSystemWatcher(null);
|
||||
|
||||
public IFileSystemWatcher CreateNew(string path) => new TestFileSystemWatcher(path);
|
||||
|
||||
public IFileSystemWatcher CreateNew(string path, string filter) => new TestFileSystemWatcher(path, filter);
|
||||
|
||||
public IFileSystemWatcher FromPath(string path) => new TestFileSystemWatcher(path);
|
||||
}
|
||||
}
|
||||
115
src/modules/Hosts/Hosts/App.xaml
Normal file
115
src/modules/Hosts/Hosts/App.xaml
Normal file
@@ -0,0 +1,115 @@
|
||||
<Application
|
||||
x:Class="Hosts.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Hosts">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
|
||||
<SolidColorBrush x:Key="SubtleButtonBackground" Color="{ThemeResource SubtleFillColorTransparent}" />
|
||||
<SolidColorBrush x:Key="SubtleButtonBackgroundPointerOver" Color="{ThemeResource SubtleFillColorSecondary}" />
|
||||
<SolidColorBrush x:Key="SubtleButtonBackgroundPressed" Color="{ThemeResource SubtleFillColorTertiary}" />
|
||||
<SolidColorBrush x:Key="SubtleButtonBackgroundDisabled" Color="{ThemeResource ControlFillColorDisabled}" />
|
||||
|
||||
<SolidColorBrush x:Key="SubtleButtonForeground" Color="{ThemeResource TextFillColorPrimary}" />
|
||||
<SolidColorBrush x:Key="SubtleButtonForegroundPointerOver" Color="{ThemeResource TextFillColorPrimary}" />
|
||||
<SolidColorBrush x:Key="SubtleButtonForegroundPressed" Color="{ThemeResource TextFillColorSecondary}" />
|
||||
<SolidColorBrush x:Key="SubtleButtonForegroundDisabled" Color="{ThemeResource TextFillColorDisabled}" />
|
||||
|
||||
|
||||
<Style x:Key="SubtleButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{ThemeResource SubtleButtonBackground}" />
|
||||
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource SubtleButtonForeground}" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="{StaticResource ButtonPadding}" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
|
||||
<Setter Property="FocusVisualMargin" Value="-3" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
AnimatedIcon.State="Normal"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Background="{TemplateBinding Background}"
|
||||
BackgroundSizing="{TemplateBinding BackgroundSizing}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<ContentPresenter.BackgroundTransition>
|
||||
<BrushTransition Duration="0:0:0.083" />
|
||||
</ContentPresenter.BackgroundTransition>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="PointerOver" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
|
||||
<VisualState x:Name="Pressed">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Pressed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
|
||||
<VisualState x:Name="Disabled">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Normal" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</ContentPresenter>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
113
src/modules/Hosts/Hosts/App.xaml.cs
Normal file
113
src/modules/Hosts/Hosts/App.xaml.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
// 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.IO.Abstractions;
|
||||
using System.Threading;
|
||||
using Hosts.Helpers;
|
||||
using Hosts.Settings;
|
||||
using Hosts.ViewModels;
|
||||
using Hosts.Views;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Hosts
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
private Window _window;
|
||||
|
||||
public IHost Host
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public static T GetService<T>()
|
||||
where T : class
|
||||
{
|
||||
if ((App.Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
|
||||
{
|
||||
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Host = Microsoft.Extensions.Hosting.Host.
|
||||
CreateDefaultBuilder().
|
||||
UseContentRoot(AppContext.BaseDirectory).
|
||||
ConfigureServices((context, services) =>
|
||||
{
|
||||
// Core Services
|
||||
services.AddSingleton<IFileSystem, FileSystem>();
|
||||
services.AddSingleton<IHostsService, HostsService>();
|
||||
services.AddSingleton<IUserSettings, UserSettings>();
|
||||
services.AddSingleton<IElevationHelper, ElevationHelper>();
|
||||
|
||||
// Views and ViewModels
|
||||
services.AddTransient<MainPage>();
|
||||
services.AddTransient<MainViewModel>();
|
||||
}).
|
||||
Build();
|
||||
|
||||
UnhandledException += App_UnhandledException;
|
||||
|
||||
new Thread(() =>
|
||||
{
|
||||
// Delete old backups only if running elevated
|
||||
if (!GetService<IElevationHelper>().IsElevated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
GetService<IHostsService>().CleanupBackup();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to delete backup", ex);
|
||||
}
|
||||
}).Start();
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
var cmdArgs = Environment.GetCommandLineArgs();
|
||||
if (cmdArgs?.Length > 1)
|
||||
{
|
||||
if (int.TryParse(cmdArgs[cmdArgs.Length - 1], out int powerToysRunnerPid))
|
||||
{
|
||||
Logger.LogInfo($"Hosts started from the PowerToys Runner. Runner pid={powerToysRunnerPid}");
|
||||
|
||||
var dispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () =>
|
||||
{
|
||||
Logger.LogInfo("PowerToys Runner exited. Exiting Hosts");
|
||||
dispatcher.TryEnqueue(App.Current.Exit);
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo($"Hosts started detached from PowerToys Runner.");
|
||||
}
|
||||
|
||||
_window = new MainWindow();
|
||||
_window.Activate();
|
||||
}
|
||||
|
||||
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.LogError("Unhandled exception", e.Exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/modules/Hosts/Hosts/Helpers/ElevationHelper.cs
Normal file
20
src/modules/Hosts/Hosts/Helpers/ElevationHelper.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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.Security.Principal;
|
||||
|
||||
namespace Hosts.Helpers
|
||||
{
|
||||
public class ElevationHelper : IElevationHelper
|
||||
{
|
||||
private readonly bool _isElevated;
|
||||
|
||||
public bool IsElevated => _isElevated;
|
||||
|
||||
public ElevationHelper()
|
||||
{
|
||||
_isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
}
|
||||
}
|
||||
238
src/modules/Hosts/Hosts/Helpers/HostsService.cs
Normal file
238
src/modules/Hosts/Hosts/Helpers/HostsService.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
// 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.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 Hosts.Models;
|
||||
using Hosts.Settings;
|
||||
using Settings.UI.Library.Enumerations;
|
||||
|
||||
namespace Hosts.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 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<(string Unparsed, List<Entry> Entries)> ReadAsync()
|
||||
{
|
||||
var entries = new List<Entry>();
|
||||
var unparsedBuilder = new StringBuilder();
|
||||
|
||||
if (!Exists())
|
||||
{
|
||||
return (unparsedBuilder.ToString(), entries);
|
||||
}
|
||||
|
||||
var lines = await _fileSystem.File.ReadAllLinesAsync(HostsFilePath);
|
||||
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entry = new Entry(line);
|
||||
|
||||
if (entry.Valid)
|
||||
{
|
||||
entries.Add(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (unparsedBuilder.Length > 0)
|
||||
{
|
||||
unparsedBuilder.Append(Environment.NewLine);
|
||||
}
|
||||
|
||||
unparsedBuilder.Append(line);
|
||||
}
|
||||
}
|
||||
|
||||
return (unparsedBuilder.ToString(), entries);
|
||||
}
|
||||
|
||||
public async Task<bool> WriteAsync(string additionalLines, IEnumerable<Entry> entries)
|
||||
{
|
||||
if (!_elevationHelper.IsElevated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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.GetLine());
|
||||
}
|
||||
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 == AdditionalLinesPosition.Top)
|
||||
{
|
||||
lines.Insert(0, additionalLines);
|
||||
}
|
||||
else if (_userSettings.AdditionalLinesPosition == AdditionalLinesPosition.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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to write hosts file", ex);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fileSystemWatcher.EnableRaisingEvents = true;
|
||||
_asyncLock.Release();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/modules/Hosts/Hosts/Helpers/IElevationHelper.cs
Normal file
11
src/modules/Hosts/Hosts/Helpers/IElevationHelper.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
// 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.
|
||||
|
||||
namespace Hosts.Helpers
|
||||
{
|
||||
public interface IElevationHelper
|
||||
{
|
||||
bool IsElevated { get; }
|
||||
}
|
||||
}
|
||||
26
src/modules/Hosts/Hosts/Helpers/IHostsService.cs
Normal file
26
src/modules/Hosts/Hosts/Helpers/IHostsService.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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.Threading.Tasks;
|
||||
using Hosts.Models;
|
||||
|
||||
namespace Hosts.Helpers
|
||||
{
|
||||
public interface IHostsService : IDisposable
|
||||
{
|
||||
string HostsFilePath { get; }
|
||||
|
||||
event EventHandler FileChanged;
|
||||
|
||||
Task<(string Unparsed, List<Entry> Entries)> ReadAsync();
|
||||
|
||||
Task<bool> WriteAsync(string additionalLines, IEnumerable<Entry> entries);
|
||||
|
||||
Task<bool> PingAsync(string address);
|
||||
|
||||
void CleanupBackup();
|
||||
}
|
||||
}
|
||||
80
src/modules/Hosts/Hosts/Helpers/Logger.cs
Normal file
80
src/modules/Hosts/Hosts/Helpers/Logger.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using interop;
|
||||
|
||||
namespace Hosts.Helpers
|
||||
{
|
||||
// TODO: use centralized logger https://github.com/microsoft/PowerToys/issues/19650
|
||||
public static class Logger
|
||||
{
|
||||
private static readonly IFileSystem _fileSystem = new FileSystem();
|
||||
private static readonly string ApplicationLogPath = Path.Combine(Constants.AppDataPath(), "Hosts\\Logs");
|
||||
|
||||
static Logger()
|
||||
{
|
||||
if (!_fileSystem.Directory.Exists(ApplicationLogPath))
|
||||
{
|
||||
_fileSystem.Directory.CreateDirectory(ApplicationLogPath);
|
||||
}
|
||||
|
||||
// Using InvariantCulture since this is used for a log file name
|
||||
var logFilePath = _fileSystem.Path.Combine(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt");
|
||||
|
||||
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
|
||||
|
||||
Trace.AutoFlush = true;
|
||||
}
|
||||
|
||||
public static void LogError(string message)
|
||||
{
|
||||
Log(message, "ERROR");
|
||||
}
|
||||
|
||||
public static void LogError(string message, Exception ex)
|
||||
{
|
||||
Log(
|
||||
message + Environment.NewLine +
|
||||
ex?.Message + Environment.NewLine +
|
||||
"Inner exception: " + Environment.NewLine +
|
||||
ex?.InnerException?.Message + Environment.NewLine +
|
||||
"Stack trace: " + Environment.NewLine +
|
||||
ex?.StackTrace,
|
||||
"ERROR");
|
||||
}
|
||||
|
||||
public static void LogWarning(string message)
|
||||
{
|
||||
Log(message, "WARNING");
|
||||
}
|
||||
|
||||
public static void LogInfo(string message)
|
||||
{
|
||||
Log(message, "INFO");
|
||||
}
|
||||
|
||||
private static void Log(string message, string type)
|
||||
{
|
||||
Trace.WriteLine(type + ": " + DateTime.Now.TimeOfDay);
|
||||
Trace.Indent();
|
||||
Trace.WriteLine(GetCallerInfo());
|
||||
Trace.WriteLine(message);
|
||||
Trace.Unindent();
|
||||
}
|
||||
|
||||
private static string GetCallerInfo()
|
||||
{
|
||||
StackTrace stackTrace = new StackTrace();
|
||||
|
||||
var methodName = stackTrace.GetFrame(3)?.GetMethod();
|
||||
var className = methodName?.DeclaringType.Name;
|
||||
return "[Method]: " + methodName?.Name + " [Class]: " + className;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/modules/Hosts/Hosts/Helpers/NativeMethods.cs
Normal file
24
src/modules/Hosts/Hosts/Helpers/NativeMethods.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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.
|
||||
|
||||
namespace Hosts.Helpers
|
||||
{
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
internal class NativeMethods
|
||||
{
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern IntPtr SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr processId);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
|
||||
}
|
||||
}
|
||||
14
src/modules/Hosts/Hosts/Helpers/StringHelper.cs
Normal file
14
src/modules/Hosts/Hosts/Helpers/StringHelper.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// 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.
|
||||
|
||||
namespace Hosts.Helpers
|
||||
{
|
||||
public static class StringHelper
|
||||
{
|
||||
public static string GetHostsWithComment(string hosts, string comment)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(comment) ? hosts : string.Concat(hosts, " - ", comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/modules/Hosts/Hosts/Helpers/ValidationHelper.cs
Normal file
60
src/modules/Hosts/Hosts/Helpers/ValidationHelper.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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.Text.RegularExpressions;
|
||||
|
||||
namespace Hosts.Helpers
|
||||
{
|
||||
public static class ValidationHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the address is a valid IPv4
|
||||
/// </summary>
|
||||
public static bool ValidIPv4(string address)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(address))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var regex = new Regex("^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
|
||||
return regex.IsMatch(address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the address is a valid IPv6
|
||||
/// </summary>
|
||||
public static bool ValidIPv6(string address)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(address))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var regex = new Regex("^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$");
|
||||
return regex.IsMatch(address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the hosts are valid
|
||||
/// </summary>
|
||||
public static bool ValidHosts(string hosts)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hosts))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var host in hosts.Split(' '))
|
||||
{
|
||||
if (System.Uri.CheckHostName(host) == System.UriHostNameType.Unknown)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/modules/Hosts/Hosts/Hosts.csproj
Normal file
55
src/modules/Hosts/Hosts/Hosts.csproj
Normal file
@@ -0,0 +1,55 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\Version.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>Hosts</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<RuntimeIdentifiers>win10-x64;win10-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\modules\$(AssemblyName)</OutputPath>
|
||||
<IntermediateOutputPath>$(SolutionDir)$(Platform)\$(Configuration)\obj\$(AssemblyName)</IntermediateOutputPath>
|
||||
<RootNamespace>Hosts</RootNamespace>
|
||||
<AssemblyName>PowerToys.Hosts</AssemblyName>
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
|
||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="icon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.0.0" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.5" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22000.197" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.8" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="12.2.5" />
|
||||
<PackageReference Include="WinUIEx" Version="1.8.0" />
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnablePreviewMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Views\MainPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
37
src/modules/Hosts/Hosts/MainWindow.xaml
Normal file
37
src/modules/Hosts/Hosts/MainWindow.xaml
Normal file
@@ -0,0 +1,37 @@
|
||||
<winuiex:WindowEx
|
||||
x:Uid="Window"
|
||||
x:Class="Hosts.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Hosts"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="using:Hosts.Views"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Width="680"
|
||||
MinWidth="480"
|
||||
MinHeight="320"
|
||||
mc:Ignorable="d">
|
||||
<winuiex:WindowEx.Backdrop>
|
||||
<winuiex:MicaSystemBackdrop />
|
||||
</winuiex:WindowEx.Backdrop>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="32" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid x:Name="titleBar">
|
||||
<StackPanel
|
||||
Margin="16,8,8,8"
|
||||
VerticalAlignment="Top"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock FontFamily="{ThemeResource SymbolThemeFontFamily}" Text="" />
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="Hosts File Editor" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<views:MainPage Grid.Row="1" />
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
71
src/modules/Hosts/Hosts/MainWindow.xaml.cs
Normal file
71
src/modules/Hosts/Hosts/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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 Hosts.Helpers;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Hosts
|
||||
{
|
||||
public sealed partial class MainWindow : WindowEx
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
if (AppWindowTitleBar.IsCustomizationSupported())
|
||||
{
|
||||
SetTitleBar();
|
||||
}
|
||||
else
|
||||
{
|
||||
titleBar.Visibility = Visibility.Collapsed;
|
||||
|
||||
// Set window icon
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
|
||||
AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
appWindow.SetIcon("icon.ico");
|
||||
|
||||
if (ThemeHelpers.GetAppTheme() == AppTheme.Dark)
|
||||
{
|
||||
ThemeHelpers.SetImmersiveDarkMode(hWnd, true);
|
||||
}
|
||||
}
|
||||
|
||||
BringToForeground();
|
||||
}
|
||||
|
||||
private void SetTitleBar()
|
||||
{
|
||||
AppWindow window = this.GetAppWindow();
|
||||
window.TitleBar.ExtendsContentIntoTitleBar = true;
|
||||
window.TitleBar.ButtonBackgroundColor = Colors.Transparent;
|
||||
SetTitleBar(titleBar);
|
||||
}
|
||||
|
||||
private void BringToForeground()
|
||||
{
|
||||
var handle = this.GetWindowHandle();
|
||||
var fgHandle = NativeMethods.GetForegroundWindow();
|
||||
|
||||
var threadId1 = NativeMethods.GetWindowThreadProcessId(handle, System.IntPtr.Zero);
|
||||
var threadId2 = NativeMethods.GetWindowThreadProcessId(fgHandle, System.IntPtr.Zero);
|
||||
|
||||
if (threadId1 != threadId2)
|
||||
{
|
||||
NativeMethods.AttachThreadInput(threadId1, threadId2, true);
|
||||
NativeMethods.SetForegroundWindow(handle);
|
||||
NativeMethods.AttachThreadInput(threadId1, threadId2, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
NativeMethods.SetForegroundWindow(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
149
src/modules/Hosts/Hosts/Models/Entry.cs
Normal file
149
src/modules/Hosts/Hosts/Models/Entry.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
// 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.Net;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Hosts.Helpers;
|
||||
|
||||
namespace Hosts.Models
|
||||
{
|
||||
public partial class Entry : ObservableObject
|
||||
{
|
||||
private string _line;
|
||||
|
||||
private string _address;
|
||||
|
||||
public string Address
|
||||
{
|
||||
get => _address;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _address, value);
|
||||
OnPropertyChanged(nameof(Valid));
|
||||
}
|
||||
}
|
||||
|
||||
private string _hosts;
|
||||
|
||||
public string Hosts
|
||||
{
|
||||
get => _hosts;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _hosts, value);
|
||||
OnPropertyChanged(nameof(Valid));
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private string _comment;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _active;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool? _ping;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _pinging;
|
||||
|
||||
public bool Valid => ValidationHelper.ValidHosts(_hosts) && (ValidationHelper.ValidIPv4(_address) || ValidationHelper.ValidIPv6(_address));
|
||||
|
||||
public Entry()
|
||||
{
|
||||
}
|
||||
|
||||
public Entry(string line)
|
||||
{
|
||||
_line = line.Trim();
|
||||
Parse();
|
||||
}
|
||||
|
||||
public Entry(string address, string hosts, string comment, bool active)
|
||||
{
|
||||
Address = address.Trim();
|
||||
Hosts = hosts.Trim();
|
||||
Comment = comment.Trim();
|
||||
Active = active;
|
||||
}
|
||||
|
||||
public void Parse()
|
||||
{
|
||||
Active = !_line.StartsWith("#", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
var lineSplit = _line.TrimStart(' ', '#').Split('#');
|
||||
|
||||
if (lineSplit.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var addressHost = lineSplit[0];
|
||||
|
||||
var addressHostSplit = addressHost.Split(' ', '\t');
|
||||
var hostsBuilder = new StringBuilder();
|
||||
var commentBuilder = new StringBuilder();
|
||||
|
||||
for (var i = 0; i < addressHostSplit.Length; i++)
|
||||
{
|
||||
var element = addressHostSplit[i].Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(element))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Address == null)
|
||||
{
|
||||
if (IPAddress.TryParse(element, out var _) && (element.Contains(':') || element.Contains('.')))
|
||||
{
|
||||
Address = element;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hostsBuilder.Length > 0)
|
||||
{
|
||||
hostsBuilder.Append(' ');
|
||||
}
|
||||
|
||||
hostsBuilder.Append(element);
|
||||
}
|
||||
}
|
||||
|
||||
Hosts = hostsBuilder.ToString();
|
||||
|
||||
for (var i = 1; i < lineSplit.Length; i++)
|
||||
{
|
||||
if (commentBuilder.Length > 0)
|
||||
{
|
||||
commentBuilder.Append('#');
|
||||
}
|
||||
|
||||
commentBuilder.Append(lineSplit[i]);
|
||||
}
|
||||
|
||||
Comment = commentBuilder.ToString().Trim();
|
||||
}
|
||||
|
||||
public Entry Clone()
|
||||
{
|
||||
return new Entry
|
||||
{
|
||||
_line = _line,
|
||||
Address = Address,
|
||||
Hosts = Hosts,
|
||||
Comment = Comment,
|
||||
Active = Active,
|
||||
};
|
||||
}
|
||||
|
||||
public string GetLine()
|
||||
{
|
||||
return _line;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/modules/Hosts/Hosts/Program.cs
Normal file
38
src/modules/Hosts/Hosts/Program.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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.Threading;
|
||||
using Hosts.Helpers;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
|
||||
namespace Hosts
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
var instanceKey = AppInstance.FindOrRegisterForKey("PowerToys_Hosts_Instance");
|
||||
|
||||
if (instanceKey.IsCurrent)
|
||||
{
|
||||
Microsoft.UI.Xaml.Application.Start((p) =>
|
||||
{
|
||||
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
_ = new App();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Another instance of Hosts running. Exiting Hosts");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/modules/Hosts/Hosts/Settings/IUserSettings.cs
Normal file
15
src/modules/Hosts/Hosts/Settings/IUserSettings.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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 Settings.UI.Library.Enumerations;
|
||||
|
||||
namespace Hosts.Settings
|
||||
{
|
||||
public interface IUserSettings
|
||||
{
|
||||
public bool ShowStartupWarning { get; }
|
||||
|
||||
public AdditionalLinesPosition AdditionalLinesPosition { get; }
|
||||
}
|
||||
}
|
||||
81
src/modules/Hosts/Hosts/Settings/UserSettings.cs
Normal file
81
src/modules/Hosts/Hosts/Settings/UserSettings.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
// 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.IO.Abstractions;
|
||||
using System.Threading;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
|
||||
using Settings.UI.Library.Enumerations;
|
||||
|
||||
namespace Hosts.Settings
|
||||
{
|
||||
public class UserSettings : IUserSettings
|
||||
{
|
||||
private const string HostsModuleName = "Hosts";
|
||||
private const int MaxNumberOfRetry = 5;
|
||||
|
||||
private readonly ISettingsUtils _settingsUtils;
|
||||
private readonly IFileSystemWatcher _watcher;
|
||||
private readonly object _loadingSettingsLock = new object();
|
||||
|
||||
public bool ShowStartupWarning { get; private set; }
|
||||
|
||||
public AdditionalLinesPosition AdditionalLinesPosition { get; private set; }
|
||||
|
||||
public UserSettings()
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
ShowStartupWarning = true;
|
||||
AdditionalLinesPosition = AdditionalLinesPosition.Top;
|
||||
|
||||
LoadSettingsFromJson();
|
||||
|
||||
_watcher = Helper.GetFileWatcher(HostsModuleName, "settings.json", () => LoadSettingsFromJson());
|
||||
}
|
||||
|
||||
private void LoadSettingsFromJson()
|
||||
{
|
||||
lock (_loadingSettingsLock)
|
||||
{
|
||||
var retry = true;
|
||||
var retryCount = 0;
|
||||
|
||||
while (retry)
|
||||
{
|
||||
try
|
||||
{
|
||||
retryCount++;
|
||||
|
||||
if (!_settingsUtils.SettingsExists(HostsModuleName))
|
||||
{
|
||||
Logger.LogInfo("Hosts settings.json was missing, creating a new one");
|
||||
var defaultSettings = new HostsSettings();
|
||||
defaultSettings.Save(_settingsUtils);
|
||||
}
|
||||
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<HostsSettings>(HostsModuleName);
|
||||
if (settings != null)
|
||||
{
|
||||
ShowStartupWarning = settings.Properties.ShowStartupWarning;
|
||||
AdditionalLinesPosition = settings.Properties.AdditionalLinesPosition;
|
||||
}
|
||||
|
||||
retry = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (retryCount > MaxNumberOfRetry)
|
||||
{
|
||||
retry = false;
|
||||
}
|
||||
|
||||
Logger.LogError("Failed to read changed settings", ex);
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
261
src/modules/Hosts/Hosts/Strings/en-us/Resources.resw
Normal file
261
src/modules/Hosts/Hosts/Strings/en-us/Resources.resw
Normal file
@@ -0,0 +1,261 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Active.Header" xml:space="preserve">
|
||||
<value>Active</value>
|
||||
</data>
|
||||
<data name="ActiveToggle.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Active</value>
|
||||
</data>
|
||||
<data name="AddBtn" xml:space="preserve">
|
||||
<value>Add</value>
|
||||
</data>
|
||||
<data name="AddEntry.Text" xml:space="preserve">
|
||||
<value>New entry</value>
|
||||
</data>
|
||||
<data name="AddEntryBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>New entry</value>
|
||||
</data>
|
||||
<data name="AdditionalLinesBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Additional lines</value>
|
||||
</data>
|
||||
<data name="AdditionalLinesBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Additional lines</value>
|
||||
</data>
|
||||
<data name="AdditionalLinesDialog.CloseButtonText" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="AdditionalLinesDialog.PrimaryButtonText" xml:space="preserve">
|
||||
<value>Save</value>
|
||||
</data>
|
||||
<data name="AdditionalLinesDialog.Title" xml:space="preserve">
|
||||
<value>Additional lines</value>
|
||||
</data>
|
||||
<data name="AddNewEntryDialog_Title" xml:space="preserve">
|
||||
<value>Add new entry</value>
|
||||
</data>
|
||||
<data name="Address.Header" xml:space="preserve">
|
||||
<value>Address</value>
|
||||
<comment>"Address" refers to the IP address of the entry</comment>
|
||||
</data>
|
||||
<data name="AddressFilter.Header" xml:space="preserve">
|
||||
<value>Address</value>
|
||||
<comment>"Address" refers to the IP address of the entry</comment>
|
||||
</data>
|
||||
<data name="ClearFiltersBtn.Content" xml:space="preserve">
|
||||
<value>Clear filters</value>
|
||||
</data>
|
||||
<data name="ClearFiltersBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Clear filters</value>
|
||||
</data>
|
||||
<data name="Comment.Header" xml:space="preserve">
|
||||
<value>Comment</value>
|
||||
<comment>"Comment" refers to the comment of the entry</comment>
|
||||
</data>
|
||||
<data name="CommentFilter.Header" xml:space="preserve">
|
||||
<value>Comment</value>
|
||||
<comment>"Comment" refers to the comment of the entry</comment>
|
||||
</data>
|
||||
<data name="Delete.Text" xml:space="preserve">
|
||||
<value>Delete</value>
|
||||
</data>
|
||||
<data name="DeleteDialog.CloseButtonText" xml:space="preserve">
|
||||
<value>No</value>
|
||||
</data>
|
||||
<data name="DeleteDialog.PrimaryButtonText" xml:space="preserve">
|
||||
<value>Yes</value>
|
||||
</data>
|
||||
<data name="DeleteDialogAreYouSure.Text" xml:space="preserve">
|
||||
<value>Are you sure you want to delete this entry?</value>
|
||||
</data>
|
||||
<data name="Entries.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Entries</value>
|
||||
</data>
|
||||
<data name="EntryDialog.CloseButtonText" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="FileChanged.Message" xml:space="preserve">
|
||||
<value>Hosts file has changed by another application. Do you want to reload it?</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="FileSaveError.Message" xml:space="preserve">
|
||||
<value>Failed to save hosts file.</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>
|
||||
<data name="FilterBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Filters</value>
|
||||
</data>
|
||||
<data name="Hosts.Header" xml:space="preserve">
|
||||
<value>Hosts</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="Hosts.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Seperate multiple hosts by space (e.g. server server.local).</value>
|
||||
<comment>Do not localize "server" and "server.local"</comment>
|
||||
</data>
|
||||
<data name="HostsFilter.Header" xml:space="preserve">
|
||||
<value>Hosts</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="MoveDown.Text" xml:space="preserve">
|
||||
<value>Move down</value>
|
||||
</data>
|
||||
<data name="MoveUp.Text" xml:space="preserve">
|
||||
<value>Move up</value>
|
||||
</data>
|
||||
<data name="Ping.Text" xml:space="preserve">
|
||||
<value>Ping</value>
|
||||
<comment>"Ping" refers to the command-line utility, do not loc</comment>
|
||||
</data>
|
||||
<data name="Reload.Content" xml:space="preserve">
|
||||
<value>Reload</value>
|
||||
</data>
|
||||
<data name="SettingsBtn.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="SettingsBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="UpdateBtn" xml:space="preserve">
|
||||
<value>Update</value>
|
||||
</data>
|
||||
<data name="UpdateEntry_Title" xml:space="preserve">
|
||||
<value>Update the entry</value>
|
||||
</data>
|
||||
<data name="WarningDialog_AcceptBtn" xml:space="preserve">
|
||||
<value>Accept</value>
|
||||
</data>
|
||||
<data name="WarningDialog_QuitBtn" xml:space="preserve">
|
||||
<value>Quit</value>
|
||||
</data>
|
||||
<data name="WarningDialog_Text" xml:space="preserve">
|
||||
<value>Altering hosts file has direct real world impact of how this computer resolves domain names.</value>
|
||||
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
<data name="WarningDialog_Title" xml:space="preserve">
|
||||
<value>Warning</value>
|
||||
</data>
|
||||
<data name="Window.Title" xml:space="preserve">
|
||||
<value>Hosts File Editor</value>
|
||||
<comment>"Hosts File Editor" is the name of the utility. "Hosts" refers to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
</root>
|
||||
242
src/modules/Hosts/Hosts/ViewModels/MainViewModel.cs
Normal file
242
src/modules/Hosts/Hosts/ViewModels/MainViewModel.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
// 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.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Common.UI;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Hosts.Helpers;
|
||||
using Hosts.Models;
|
||||
using Hosts.Settings;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace Hosts.ViewModels
|
||||
{
|
||||
public partial class MainViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IHostsService _hostsService;
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
private bool _disposed;
|
||||
|
||||
[ObservableProperty]
|
||||
private Entry _selected;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _error;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _fileChanged;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _addressFilter;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _hostsFilter;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _commentFilter;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _filtered;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _additionalLines;
|
||||
|
||||
private ObservableCollection<Entry> _entries;
|
||||
|
||||
public ObservableCollection<Entry> Entries
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_filtered)
|
||||
{
|
||||
var filter = _entries.AsEnumerable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_addressFilter))
|
||||
{
|
||||
filter = filter.Where(e => e.Address.Contains(_addressFilter, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_hostsFilter))
|
||||
{
|
||||
filter = filter.Where(e => e.Hosts.Contains(_hostsFilter, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_commentFilter))
|
||||
{
|
||||
filter = filter.Where(e => e.Comment.Contains(_commentFilter, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
return new ObservableCollection<Entry>(filter);
|
||||
}
|
||||
else
|
||||
{
|
||||
return _entries;
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_entries = value;
|
||||
OnPropertyChanged(nameof(Entries));
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand ReadHostsCommand => new RelayCommand(ReadHosts);
|
||||
|
||||
public ICommand ApplyFiltersCommand => new RelayCommand(ApplyFilters);
|
||||
|
||||
public ICommand ClearFiltersCommand => new RelayCommand(ClearFilters);
|
||||
|
||||
public ICommand OpenSettingsCommand => new RelayCommand(OpenSettings);
|
||||
|
||||
public MainViewModel(
|
||||
IHostsService hostService,
|
||||
IUserSettings userSettings)
|
||||
{
|
||||
_hostsService = hostService;
|
||||
_userSettings = userSettings;
|
||||
|
||||
_hostsService.FileChanged += (s, e) =>
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() => FileChanged = true);
|
||||
};
|
||||
}
|
||||
|
||||
public void Add(Entry entry)
|
||||
{
|
||||
entry.PropertyChanged += Entry_PropertyChanged;
|
||||
_entries.Add(entry);
|
||||
}
|
||||
|
||||
public void Update(int index, Entry entry)
|
||||
{
|
||||
var existingEntry = _entries.ElementAt(index);
|
||||
existingEntry.Address = entry.Address;
|
||||
existingEntry.Comment = entry.Comment;
|
||||
existingEntry.Hosts = entry.Hosts;
|
||||
existingEntry.Active = entry.Active;
|
||||
}
|
||||
|
||||
public void DeleteSelected()
|
||||
{
|
||||
_entries.Remove(Selected);
|
||||
if (Filtered)
|
||||
{
|
||||
OnPropertyChanged(nameof(Entries));
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateAdditionalLines(string lines)
|
||||
{
|
||||
_additionalLines = lines;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var error = !await _hostsService.WriteAsync(_additionalLines, _entries);
|
||||
await _dispatcherQueue.EnqueueAsync(() => Error = error);
|
||||
});
|
||||
}
|
||||
|
||||
public void ReadHosts()
|
||||
{
|
||||
FileChanged = false;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
(_additionalLines, var entries) = await _hostsService.ReadAsync();
|
||||
|
||||
await _dispatcherQueue.EnqueueAsync(() =>
|
||||
{
|
||||
Entries = new ObservableCollection<Entry>(entries);
|
||||
|
||||
foreach (var e in _entries)
|
||||
{
|
||||
e.PropertyChanged += Entry_PropertyChanged;
|
||||
}
|
||||
|
||||
_entries.CollectionChanged += Entries_CollectionChanged;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void ApplyFilters()
|
||||
{
|
||||
if (_entries != null)
|
||||
{
|
||||
Filtered = !string.IsNullOrWhiteSpace(_addressFilter) || !string.IsNullOrWhiteSpace(_hostsFilter) || !string.IsNullOrWhiteSpace(_commentFilter);
|
||||
OnPropertyChanged(nameof(Entries));
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearFilters()
|
||||
{
|
||||
AddressFilter = null;
|
||||
HostsFilter = null;
|
||||
CommentFilter = null;
|
||||
}
|
||||
|
||||
public async Task PingSelectedAsync()
|
||||
{
|
||||
var selected = _selected;
|
||||
selected.Ping = null;
|
||||
selected.Pinging = true;
|
||||
selected.Ping = await _hostsService.PingAsync(_selected.Address);
|
||||
selected.Pinging = false;
|
||||
}
|
||||
|
||||
public void OpenSettings()
|
||||
{
|
||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Hosts);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
// Ping should't trigger a file save
|
||||
if (e.PropertyName == nameof(Entry.Ping) || e.PropertyName == nameof(Entry.Pinging))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var error = !await _hostsService.WriteAsync(_additionalLines, _entries);
|
||||
await _dispatcherQueue.EnqueueAsync(() => Error = error);
|
||||
});
|
||||
}
|
||||
|
||||
private void Entries_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var error = !await _hostsService.WriteAsync(_additionalLines, _entries);
|
||||
await _dispatcherQueue.EnqueueAsync(() => Error = error);
|
||||
});
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_hostsService?.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
366
src/modules/Hosts/Hosts/Views/MainPage.xaml
Normal file
366
src/modules/Hosts/Hosts/Views/MainPage.xaml
Normal file
@@ -0,0 +1,366 @@
|
||||
<Page
|
||||
x:Class="Hosts.Views.MainPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.UI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:helpers="using:Hosts.Helpers"
|
||||
xmlns:i="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
|
||||
xmlns:local="using:Hosts.Views"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:Hosts.Models"
|
||||
x:Name="Page"
|
||||
Loaded="Page_Loaded"
|
||||
mc:Ignorable="d">
|
||||
<i:Interaction.Behaviors>
|
||||
<ic:EventTriggerBehavior EventName="Loaded">
|
||||
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ReadHostsCommand}" />
|
||||
</ic:EventTriggerBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
|
||||
<Page.Resources>
|
||||
<converters:StringVisibilityConverter
|
||||
x:Key="StringVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="64" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid>
|
||||
<Button
|
||||
x:Uid="AddEntryBtn"
|
||||
Height="36"
|
||||
Margin="16,0,0,0"
|
||||
Command="{x:Bind NewDialogCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<TextBlock
|
||||
x:Name="Icon"
|
||||
Margin="0,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
Text="" />
|
||||
<TextBlock x:Uid="AddEntry" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<StackPanel
|
||||
Padding="0,0,16,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
x:Uid="AdditionalLinesBtn"
|
||||
Height="36"
|
||||
Command="{x:Bind AdditionalLinesDialogCommand}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<FontIcon
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
FontSize="14"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
x:Uid="FilterBtn"
|
||||
Height="36"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<FontIcon
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
FontSize="14"
|
||||
Glyph="" />
|
||||
<Button.Flyout>
|
||||
<Flyout>
|
||||
<StackPanel
|
||||
Width="320"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="12">
|
||||
<AutoSuggestBox
|
||||
x:Uid="AddressFilter"
|
||||
PlaceholderText=""
|
||||
QueryIcon="Find"
|
||||
Text="{x:Bind ViewModel.AddressFilter, Mode=TwoWay}">
|
||||
<i:Interaction.Behaviors>
|
||||
<ic:EventTriggerBehavior EventName="TextChanged">
|
||||
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ApplyFiltersCommand}" />
|
||||
</ic:EventTriggerBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
</AutoSuggestBox>
|
||||
<AutoSuggestBox
|
||||
x:Uid="HostsFilter"
|
||||
QueryIcon="Find"
|
||||
Text="{x:Bind ViewModel.HostsFilter, Mode=TwoWay}">
|
||||
<i:Interaction.Behaviors>
|
||||
<ic:EventTriggerBehavior EventName="TextChanged">
|
||||
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ApplyFiltersCommand}" />
|
||||
</ic:EventTriggerBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
</AutoSuggestBox>
|
||||
<AutoSuggestBox
|
||||
x:Uid="CommentFilter"
|
||||
QueryIcon="Find"
|
||||
Text="{x:Bind ViewModel.CommentFilter, Mode=TwoWay}">
|
||||
<i:Interaction.Behaviors>
|
||||
<ic:EventTriggerBehavior EventName="TextChanged">
|
||||
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ApplyFiltersCommand}" />
|
||||
</ic:EventTriggerBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
</AutoSuggestBox>
|
||||
<Button
|
||||
x:Uid="ClearFiltersBtn"
|
||||
Margin="0,6,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{x:Bind ViewModel.ClearFiltersCommand}"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
x:Uid="SettingsBtn"
|
||||
Height="36"
|
||||
Command="{x:Bind ViewModel.OpenSettingsCommand}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<FontIcon
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
FontSize="14"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Orientation="Vertical"
|
||||
Visibility="Visible">
|
||||
<InfoBar
|
||||
x:Uid="FileSaveError"
|
||||
CornerRadius="0"
|
||||
IsOpen="{x:Bind ViewModel.Error, Mode=TwoWay}"
|
||||
Severity="Error" />
|
||||
|
||||
<InfoBar
|
||||
x:Uid="FileChanged"
|
||||
IsOpen="{x:Bind ViewModel.FileChanged, Mode=TwoWay}"
|
||||
Severity="Informational">
|
||||
<InfoBar.ActionButton>
|
||||
<Button x:Uid="Reload" Command="{x:Bind ViewModel.ReadHostsCommand}" />
|
||||
</InfoBar.ActionButton>
|
||||
</InfoBar>
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
https://github.com/microsoft/microsoft-ui-xaml/issues/7690
|
||||
AllowDrop="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
|
||||
CanDragItems="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
|
||||
CanReorderItems="{x:Bind ViewModel.Filtered, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
|
||||
-->
|
||||
<ListView
|
||||
x:Uid="Entries"
|
||||
x:Name="Entries"
|
||||
Grid.Row="1"
|
||||
Margin="16,8,16,16"
|
||||
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Entries_ItemClick"
|
||||
ItemsSource="{Binding Entries, Mode=TwoWay}"
|
||||
SelectedItem="{Binding Selected, Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:Entry">
|
||||
<Grid
|
||||
AutomationProperties.Name="{x:Bind Address, Mode=OneWay}"
|
||||
Background="Transparent"
|
||||
IsRightTapEnabled="True"
|
||||
RightTapped="Grid_RightTapped">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="300" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<FlyoutBase.AttachedFlyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="Ping"
|
||||
Click="Ping_Click"
|
||||
Icon="TwoBars" />
|
||||
<MenuFlyoutItem
|
||||
x:Uid="MoveUp"
|
||||
Click="ReorderButtonUp_Click"
|
||||
IsEnabled="{Binding DataContext.Filtered, ElementName=Page, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="MoveDown"
|
||||
Click="ReorderButtonDown_Click"
|
||||
IsEnabled="{Binding DataContext.Filtered, ElementName=Page, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutSeparator />
|
||||
<MenuFlyoutItem
|
||||
x:Uid="Delete"
|
||||
Click="Delete_Click"
|
||||
Icon="Delete" />
|
||||
</MenuFlyout>
|
||||
</FlyoutBase.AttachedFlyout>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Address, Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind helpers:StringHelper.GetHostsWithComment(Hosts, Comment), Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<ProgressRing
|
||||
Grid.Column="2"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="0,0,8,0"
|
||||
IsActive="{x:Bind Pinging, Mode=OneWay}" />
|
||||
<FontIcon
|
||||
x:Name="PingIcon"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,8,0"
|
||||
FontSize="18"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
Visibility="Collapsed">
|
||||
<i:Interaction.Behaviors>
|
||||
<ic:DataTriggerBehavior
|
||||
Binding="{x:Bind Ping, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Glyph"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="" />
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Visibility"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="Visible" />
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Foreground"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="{StaticResource SystemFillColorSuccessBrush}" />
|
||||
</ic:DataTriggerBehavior>
|
||||
<ic:DataTriggerBehavior
|
||||
Binding="{x:Bind Ping, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Glyph"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="" />
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Visibility"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="Visible" />
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Foreground"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="{StaticResource SystemFillColorCriticalBrush}" />
|
||||
</ic:DataTriggerBehavior>
|
||||
<ic:DataTriggerBehavior
|
||||
Binding="{x:Bind Ping, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="{x:Null}">
|
||||
<ic:ChangePropertyAction
|
||||
PropertyName="Visibility"
|
||||
TargetObject="{Binding ElementName=PingIcon}"
|
||||
Value="Collapsed" />
|
||||
</ic:DataTriggerBehavior>
|
||||
</i:Interaction.Behaviors>
|
||||
</FontIcon>
|
||||
<ToggleSwitch
|
||||
x:Uid="ActiveToggle"
|
||||
Grid.Column="3"
|
||||
Width="40"
|
||||
MinWidth="0"
|
||||
HorizontalAlignment="Right"
|
||||
IsOn="{x:Bind Active, Mode=TwoWay}"
|
||||
OffContent=""
|
||||
OnContent="" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<ContentDialog
|
||||
x:Name="EntryDialog"
|
||||
x:Uid="EntryDialog"
|
||||
IsPrimaryButtonEnabled="{Binding Valid, Mode=TwoWay}"
|
||||
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
|
||||
<ContentDialog.DataContext>
|
||||
<models:Entry />
|
||||
</ContentDialog.DataContext>
|
||||
<StackPanel
|
||||
MinWidth="480"
|
||||
Margin="0,12,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="24">
|
||||
<TextBox
|
||||
x:Uid="Address"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{Binding Address, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox
|
||||
x:Uid="Hosts"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{Binding Hosts, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox
|
||||
x:Uid="Comment"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{Binding Comment, Mode=TwoWay}" />
|
||||
<ToggleSwitch
|
||||
x:Uid="Active"
|
||||
IsOn="{Binding Active, Mode=TwoWay}"
|
||||
OffContent=""
|
||||
OnContent="" />
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
|
||||
<ContentDialog
|
||||
x:Name="DeleteDialog"
|
||||
x:Uid="DeleteDialog"
|
||||
PrimaryButtonCommand="{x:Bind DeleteCommand}"
|
||||
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
|
||||
<TextBlock x:Uid="DeleteDialogAreYouSure" />
|
||||
</ContentDialog>
|
||||
|
||||
<ContentDialog
|
||||
x:Name="AdditionalLinesDialog"
|
||||
x:Uid="AdditionalLinesDialog"
|
||||
PrimaryButtonCommand="{x:Bind UpdateAdditionalLinesCommand}"
|
||||
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
|
||||
|
||||
<TextBox
|
||||
x:Name="AdditionalLines"
|
||||
Height="300"
|
||||
MinWidth="480"
|
||||
Margin="0,12,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
</ContentDialog>
|
||||
</Grid>
|
||||
</Page>
|
||||
185
src/modules/Hosts/Hosts/Views/MainPage.xaml.cs
Normal file
185
src/modules/Hosts/Hosts/Views/MainPage.xaml.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
// 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.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Hosts.Models;
|
||||
using Hosts.Settings;
|
||||
using Hosts.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace Hosts.Views
|
||||
{
|
||||
public sealed partial class MainPage : Page
|
||||
{
|
||||
public MainViewModel ViewModel { get; private set; }
|
||||
|
||||
public ICommand NewDialogCommand => new AsyncRelayCommand(OpenNewDialogAsync);
|
||||
|
||||
public ICommand AdditionalLinesDialogCommand => new AsyncRelayCommand(OpenAdditionalLinesDialogAsync);
|
||||
|
||||
public ICommand AddCommand => new RelayCommand(Add);
|
||||
|
||||
public ICommand UpdateCommand => new RelayCommand(Update);
|
||||
|
||||
public ICommand DeleteCommand => new RelayCommand(Delete);
|
||||
|
||||
public ICommand UpdateAdditionalLinesCommand => new RelayCommand(UpdateAdditionalLines);
|
||||
|
||||
public ICommand ExitCommand => new RelayCommand(() => { Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread().TryEnqueue(Application.Current.Exit); });
|
||||
|
||||
public MainPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
ViewModel = App.GetService<MainViewModel>();
|
||||
DataContext = ViewModel;
|
||||
}
|
||||
|
||||
private async Task OpenNewDialogAsync()
|
||||
{
|
||||
var resourceLoader = ResourceLoader.GetForViewIndependentUse();
|
||||
EntryDialog.Title = resourceLoader.GetString("AddNewEntryDialog_Title");
|
||||
EntryDialog.PrimaryButtonText = resourceLoader.GetString("AddBtn");
|
||||
EntryDialog.PrimaryButtonCommand = AddCommand;
|
||||
EntryDialog.DataContext = new Entry(string.Empty, string.Empty, string.Empty, true);
|
||||
await EntryDialog.ShowAsync();
|
||||
}
|
||||
|
||||
private async Task OpenAdditionalLinesDialogAsync()
|
||||
{
|
||||
AdditionalLines.Text = ViewModel.AdditionalLines;
|
||||
await AdditionalLinesDialog.ShowAsync();
|
||||
}
|
||||
|
||||
private async void Entries_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
var resourceLoader = ResourceLoader.GetForViewIndependentUse();
|
||||
ViewModel.Selected = e.ClickedItem as Entry;
|
||||
EntryDialog.Title = resourceLoader.GetString("UpdateEntry_Title");
|
||||
EntryDialog.PrimaryButtonText = resourceLoader.GetString("UpdateBtn");
|
||||
EntryDialog.PrimaryButtonCommand = UpdateCommand;
|
||||
var clone = ViewModel.Selected.Clone();
|
||||
EntryDialog.DataContext = clone;
|
||||
await EntryDialog.ShowAsync();
|
||||
}
|
||||
|
||||
private void Add()
|
||||
{
|
||||
ViewModel.Add(EntryDialog.DataContext as Entry);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
ViewModel.Update(Entries.SelectedIndex, EntryDialog.DataContext as Entry);
|
||||
}
|
||||
|
||||
private void Delete()
|
||||
{
|
||||
ViewModel.DeleteSelected();
|
||||
}
|
||||
|
||||
private void UpdateAdditionalLines()
|
||||
{
|
||||
ViewModel.UpdateAdditionalLines(AdditionalLines.Text);
|
||||
}
|
||||
|
||||
private void Grid_RightTapped(object sender, RightTappedRoutedEventArgs e)
|
||||
{
|
||||
var owner = sender as FrameworkElement;
|
||||
if (owner != null)
|
||||
{
|
||||
var flyoutBase = FlyoutBase.GetAttachedFlyout(owner);
|
||||
flyoutBase.ShowAt(owner, new FlyoutShowOptions
|
||||
{
|
||||
Position = e.GetPosition(owner),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async void Delete_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var menuFlyoutItem = sender as MenuFlyoutItem;
|
||||
|
||||
if (menuFlyoutItem != null)
|
||||
{
|
||||
var selectedEntry = menuFlyoutItem.DataContext as Entry;
|
||||
ViewModel.Selected = selectedEntry;
|
||||
DeleteDialog.Title = selectedEntry.Address;
|
||||
await DeleteDialog.ShowAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Ping_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var menuFlyoutItem = sender as MenuFlyoutItem;
|
||||
|
||||
if (menuFlyoutItem != null)
|
||||
{
|
||||
ViewModel.Selected = menuFlyoutItem.DataContext as Entry;
|
||||
await ViewModel.PingSelectedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Page_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var userSettings = App.GetService<IUserSettings>();
|
||||
if (userSettings.ShowStartupWarning)
|
||||
{
|
||||
var resourceLoader = ResourceLoader.GetForViewIndependentUse();
|
||||
var dialog = new ContentDialog();
|
||||
|
||||
dialog.XamlRoot = XamlRoot;
|
||||
dialog.Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style;
|
||||
dialog.Title = resourceLoader.GetString("WarningDialog_Title");
|
||||
dialog.Content = new TextBlock
|
||||
{
|
||||
Text = resourceLoader.GetString("WarningDialog_Text"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
dialog.PrimaryButtonText = resourceLoader.GetString("WarningDialog_AcceptBtn");
|
||||
dialog.PrimaryButtonStyle = Application.Current.Resources["AccentButtonStyle"] as Style;
|
||||
dialog.CloseButtonText = resourceLoader.GetString("WarningDialog_QuitBtn");
|
||||
dialog.CloseButtonCommand = ExitCommand;
|
||||
|
||||
await dialog.ShowAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReorderButtonUp_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var menuFlyoutItem = sender as MenuFlyoutItem;
|
||||
|
||||
if (menuFlyoutItem != null)
|
||||
{
|
||||
var entry = menuFlyoutItem.DataContext as Entry;
|
||||
var index = ViewModel.Entries.IndexOf(entry);
|
||||
if (index > 0)
|
||||
{
|
||||
ViewModel.Entries.Move(index, index - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReorderButtonDown_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var menuFlyoutItem = sender as MenuFlyoutItem;
|
||||
|
||||
if (menuFlyoutItem != null)
|
||||
{
|
||||
var entry = menuFlyoutItem.DataContext as Entry;
|
||||
var index = ViewModel.Entries.IndexOf(entry);
|
||||
if (index < ViewModel.Entries.Count - 1)
|
||||
{
|
||||
ViewModel.Entries.Move(index, index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/modules/Hosts/Hosts/app.manifest
Normal file
15
src/modules/Hosts/Hosts/app.manifest
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="Hosts.app"/>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<!-- The combination of below two tags have the following effect:
|
||||
1) Per-Monitor for >= Windows 10 Anniversary Update
|
||||
2) System < Windows 10 Anniversary Update
|
||||
-->
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
BIN
src/modules/Hosts/Hosts/icon.ico
Normal file
BIN
src/modules/Hosts/Hosts/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,40 @@
|
||||
#include <windows.h>
|
||||
#include "resource.h"
|
||||
#include "../../../common/version/version.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
#include "winres.h"
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION FILE_VERSION
|
||||
PRODUCTVERSION PRODUCT_VERSION
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_DLL
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
|
||||
BEGIN
|
||||
VALUE "CompanyName", COMPANY_NAME
|
||||
VALUE "FileDescription", FILE_DESCRIPTION
|
||||
VALUE "FileVersion", FILE_VERSION_STRING
|
||||
VALUE "InternalName", INTERNAL_NAME
|
||||
VALUE "LegalCopyright", COPYRIGHT_NOTE
|
||||
VALUE "OriginalFilename", ORIGINAL_FILENAME
|
||||
VALUE "ProductName", PRODUCT_NAME
|
||||
VALUE "ProductVersion", PRODUCT_VERSION_STRING
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
|
||||
END
|
||||
END
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h HostsModuleInterface.base.rc HostsModuleInterface.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{B41B888C-7DB8-4747-B262-4062E05A230D}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>HostsModuleInterface</RootNamespace>
|
||||
<ProjectName>HostsModuleInterface</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\modules\Hosts\</OutDir>
|
||||
<TargetName>PowerToys.HostsModuleInterface</TargetName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(CIBuild)'!='true'">
|
||||
<ClCompile>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<None Include="resource.base.h" />
|
||||
<ClInclude Include="Generated Files\resource.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="trace.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(CIBuild)'!='true'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="dllmain.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
|
||||
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
|
||||
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="HostsModuleInterface.base.rc" />
|
||||
<ResourceCompile Include="Generated Files\HostsModuleInterface.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Resource.resx" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Generated Files">
|
||||
<UniqueIdentifier>{875a08c6-f610-4667-bd0f-80171ed96072}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="dllmain.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="trace.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<None Include="HostsModuleInterface.base.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</None>
|
||||
<None Include="Resource.resx">
|
||||
<Filter>Resource Files</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Generated Files\resource.h">
|
||||
<Filter>Generated Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="trace.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.base.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="Generated Files\HostsModuleInterface.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
BIN
src/modules/Hosts/HostsModuleInterface/RCa04224
Normal file
BIN
src/modules/Hosts/HostsModuleInterface/RCa04224
Normal file
Binary file not shown.
124
src/modules/Hosts/HostsModuleInterface/Resource.resx
Normal file
124
src/modules/Hosts/HostsModuleInterface/Resource.resx
Normal file
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Hosts_Name" xml:space="preserve">
|
||||
<value>Hosts File Editor</value>
|
||||
<comment>"Hosts" refer to the system hosts file, do not loc</comment>
|
||||
</data>
|
||||
</root>
|
||||
199
src/modules/Hosts/HostsModuleInterface/dllmain.cpp
Normal file
199
src/modules/Hosts/HostsModuleInterface/dllmain.cpp
Normal file
@@ -0,0 +1,199 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include "trace.h"
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/utils/logger_helper.h>
|
||||
#include <interface/powertoy_module_interface.h>
|
||||
#include "Generated Files/resource.h"
|
||||
|
||||
#include <shellapi.h>
|
||||
#include <common/utils/resources.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/SettingsAPI/settings_objects.h>
|
||||
#include <string>
|
||||
|
||||
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
Trace::RegisterProvider();
|
||||
break;
|
||||
case DLL_THREAD_ATTACH:
|
||||
case DLL_THREAD_DETACH:
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
Trace::UnregisterProvider();
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// Name of the powertoy module.
|
||||
inline const std::wstring ModuleKey = L"Hosts";
|
||||
}
|
||||
|
||||
class HostsModuleInterface : public PowertoyModuleIface
|
||||
{
|
||||
private:
|
||||
bool m_enabled = false;
|
||||
|
||||
std::wstring app_name;
|
||||
|
||||
//contains the non localized key of the powertoy
|
||||
std::wstring app_key;
|
||||
|
||||
HANDLE m_hProcess;
|
||||
|
||||
bool is_process_running()
|
||||
{
|
||||
return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
|
||||
}
|
||||
|
||||
void bring_process_to_front()
|
||||
{
|
||||
auto enum_windows = [](HWND hwnd, LPARAM param) -> BOOL {
|
||||
HANDLE process_handle = (HANDLE)param;
|
||||
DWORD window_process_id = 0;
|
||||
|
||||
GetWindowThreadProcessId(hwnd, &window_process_id);
|
||||
if (GetProcessId(process_handle) == window_process_id)
|
||||
{
|
||||
SetForegroundWindow(hwnd);
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
};
|
||||
|
||||
EnumWindows(enum_windows, (LPARAM)m_hProcess);
|
||||
}
|
||||
|
||||
void launch_process(bool runas)
|
||||
{
|
||||
Logger::trace(L"Starting Hosts process");
|
||||
unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
|
||||
std::wstring executable_args = L"";
|
||||
executable_args.append(std::to_wstring(powertoys_pid));
|
||||
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
|
||||
sei.lpFile = L"modules\\Hosts\\PowerToys.Hosts.exe";
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
sei.lpParameters = executable_args.data();
|
||||
|
||||
if (runas)
|
||||
{
|
||||
sei.lpVerb = L"runas";
|
||||
}
|
||||
|
||||
if (ShellExecuteExW(&sei))
|
||||
{
|
||||
Logger::trace("Successfully started the Hosts process");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"Hosts failed to start. {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
|
||||
m_hProcess = sei.hProcess;
|
||||
}
|
||||
|
||||
public:
|
||||
HostsModuleInterface()
|
||||
{
|
||||
app_name = GET_RESOURCE_STRING(IDS_HOSTS_NAME);
|
||||
app_key = ModuleKey;
|
||||
LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::hostsLoggerName);
|
||||
}
|
||||
|
||||
~HostsModuleInterface()
|
||||
{
|
||||
m_enabled = false;
|
||||
}
|
||||
|
||||
// Destroy the powertoy and free memory
|
||||
virtual void destroy() override
|
||||
{
|
||||
Logger::trace("HostsModuleInterface::destroy()");
|
||||
delete this;
|
||||
}
|
||||
|
||||
// Return the localized display name of the powertoy
|
||||
virtual const wchar_t* get_name() override
|
||||
{
|
||||
return app_name.c_str();
|
||||
}
|
||||
|
||||
// Return the non localized key of the powertoy, this will be cached by the runner
|
||||
virtual const wchar_t* get_key() override
|
||||
{
|
||||
return app_key.c_str();
|
||||
}
|
||||
|
||||
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual void call_custom_action(const wchar_t* action) override
|
||||
{
|
||||
try
|
||||
{
|
||||
PowerToysSettings::CustomActionObject action_object =
|
||||
PowerToysSettings::CustomActionObject::from_json_string(action);
|
||||
|
||||
if (is_process_running())
|
||||
{
|
||||
bring_process_to_front();
|
||||
}
|
||||
else if (action_object.get_name() == L"Launch")
|
||||
{
|
||||
launch_process(false);
|
||||
}
|
||||
else if (action_object.get_name() == L"LaunchAdministrator")
|
||||
{
|
||||
launch_process(true);
|
||||
}
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
Logger::error(L"Failed to parse action. {}", action);
|
||||
}
|
||||
}
|
||||
|
||||
virtual void set_config(const wchar_t* config) override
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool is_enabled() override
|
||||
{
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
virtual void enable()
|
||||
{
|
||||
Logger::trace("HostsModuleInterface::enable()");
|
||||
m_enabled = true;
|
||||
};
|
||||
|
||||
virtual void disable()
|
||||
{
|
||||
Logger::trace("HostsModuleInterface::disable()");
|
||||
if (m_enabled)
|
||||
{
|
||||
TerminateProcess(m_hProcess, 1);
|
||||
}
|
||||
|
||||
m_enabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
{
|
||||
return new HostsModuleInterface();
|
||||
}
|
||||
4
src/modules/Hosts/HostsModuleInterface/packages.config
Normal file
4
src/modules/Hosts/HostsModuleInterface/packages.config
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.220418.1" targetFramework="native" />
|
||||
</packages>
|
||||
1
src/modules/Hosts/HostsModuleInterface/pch.cpp
Normal file
1
src/modules/Hosts/HostsModuleInterface/pch.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
5
src/modules/Hosts/HostsModuleInterface/pch.h
Normal file
5
src/modules/Hosts/HostsModuleInterface/pch.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <ProjectTelemetry.h>
|
||||
13
src/modules/Hosts/HostsModuleInterface/resource.base.h
Normal file
13
src/modules/Hosts/HostsModuleInterface/resource.base.h
Normal file
@@ -0,0 +1,13 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by AlwaysOnTopModuleInterface.rc
|
||||
|
||||
//////////////////////////////
|
||||
// Non-localizable
|
||||
|
||||
#define FILE_DESCRIPTION "PowerToys Hosts Module"
|
||||
#define INTERNAL_NAME "PowerToys.HostsModuleInterface"
|
||||
#define ORIGINAL_FILENAME "PowerToys.HostsModuleInterface.dll"
|
||||
|
||||
// Non-localizable
|
||||
//////////////////////////////
|
||||
19
src/modules/Hosts/HostsModuleInterface/trace.cpp
Normal file
19
src/modules/Hosts/HostsModuleInterface/trace.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "pch.h"
|
||||
#include "trace.h"
|
||||
|
||||
TRACELOGGING_DEFINE_PROVIDER(
|
||||
g_hProvider,
|
||||
"Microsoft.PowerToys",
|
||||
// {38e8889b-9731-53f5-e901-e8a7c1753074}
|
||||
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
|
||||
TraceLoggingOptionProjectTelemetry());
|
||||
|
||||
void Trace::RegisterProvider() noexcept
|
||||
{
|
||||
TraceLoggingRegister(g_hProvider);
|
||||
}
|
||||
|
||||
void Trace::UnregisterProvider() noexcept
|
||||
{
|
||||
TraceLoggingUnregister(g_hProvider);
|
||||
}
|
||||
8
src/modules/Hosts/HostsModuleInterface/trace.h
Normal file
8
src/modules/Hosts/HostsModuleInterface/trace.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Trace
|
||||
{
|
||||
public:
|
||||
static void RegisterProvider() noexcept;
|
||||
static void UnregisterProvider() noexcept;
|
||||
};
|
||||
Reference in New Issue
Block a user