diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index 9d64083237..fcd5e5c03c 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -65,6 +65,10 @@
"modules\\FileExplorerPreview\\PowerToys.SvgThumbnailProvider.dll",
"modules\\FileExplorerPreview\\PowerToys.SvgThumbnailProvider.comhost.dll",
+ "modules\\Hosts\\PowerToys.HostsModuleInterface.dll",
+ "modules\\Hosts\\PowerToys.Hosts.dll",
+ "modules\\Hosts\\PowerToys.Hosts.exe",
+
"modules\\ImageResizer\\PowerToys.ImageResizer.exe",
"modules\\ImageResizer\\PowerToys.ImageResizer.dll",
"modules\\ImageResizer\\PowerToys.ImageResizerExt.dll",
@@ -217,6 +221,7 @@
"modules\\FileExplorerPreview\\Microsoft.Web.WebView2.WinForms.dll",
"modules\\FileExplorerPreview\\Microsoft.Web.WebView2.Wpf.dll",
"modules\\FileExplorerPreview\\WebView2Loader.dll",
+ "modules\\Hosts\\Microsoft.Graphics.Canvas.Interop.dll",
"modules\\launcher\\e_sqlite3.dll",
"modules\\launcher\\LazyCache.dll",
"modules\\launcher\\SQLitePCLRaw.batteries_v2.dll",
diff --git a/.pipelines/ci/templates/build-powertoys-steps.yml b/.pipelines/ci/templates/build-powertoys-steps.yml
index 8ac45cce1b..7377106c81 100644
--- a/.pipelines/ci/templates/build-powertoys-steps.yml
+++ b/.pipelines/ci/templates/build-powertoys-steps.yml
@@ -200,6 +200,7 @@ steps:
**\PreviewPaneUnitTests.dll
**\UnitTests-SvgThumbnailProvider.dll
**\UnitTests-SvgPreviewHandler.dll
+ **\Hosts.Tests.dll
!**\obj\**
!**\ref\**
diff --git a/PowerToys.sln b/PowerToys.sln
index 831a4daa97..437de874b2 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -449,6 +449,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeasureToolUI", "src\module
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerAccentKeyboardService", "src\modules\poweraccent\PowerAccentKeyboardService\PowerAccentKeyboardService.vcxproj", "{C97D9A5D-206C-454E-997E-009E227D7F02}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts", "src\modules\Hosts\Hosts\Hosts.csproj", "{31D1C81D-765F-4446-AA62-E743F6325049}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosts", "Hosts", "{F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts.Tests", "src\modules\Hosts\Hosts.Tests\Hosts.Tests.csproj", "{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "HostsModuleInterface", "src\modules\Hosts\HostsModuleInterface\HostsModuleInterface.vcxproj", "{B41B888C-7DB8-4747-B262-4062E05A230D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -1804,6 +1812,42 @@ Global
{C97D9A5D-206C-454E-997E-009E227D7F02}.Release|x64.Build.0 = Release|x64
{C97D9A5D-206C-454E-997E-009E227D7F02}.Release|x86.ActiveCfg = Release|x64
{C97D9A5D-206C-454E-997E-009E227D7F02}.Release|x86.Build.0 = Release|x64
+ {31D1C81D-765F-4446-AA62-E743F6325049}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {31D1C81D-765F-4446-AA62-E743F6325049}.Debug|ARM64.Build.0 = Debug|ARM64
+ {31D1C81D-765F-4446-AA62-E743F6325049}.Debug|x64.ActiveCfg = Debug|x64
+ {31D1C81D-765F-4446-AA62-E743F6325049}.Debug|x64.Build.0 = Debug|x64
+ {31D1C81D-765F-4446-AA62-E743F6325049}.Debug|x86.ActiveCfg = Debug|x64
+ {31D1C81D-765F-4446-AA62-E743F6325049}.Debug|x86.Build.0 = Debug|x64
+ {31D1C81D-765F-4446-AA62-E743F6325049}.Release|ARM64.ActiveCfg = Release|ARM64
+ {31D1C81D-765F-4446-AA62-E743F6325049}.Release|ARM64.Build.0 = Release|ARM64
+ {31D1C81D-765F-4446-AA62-E743F6325049}.Release|x64.ActiveCfg = Release|x64
+ {31D1C81D-765F-4446-AA62-E743F6325049}.Release|x64.Build.0 = Release|x64
+ {31D1C81D-765F-4446-AA62-E743F6325049}.Release|x86.ActiveCfg = Release|x64
+ {31D1C81D-765F-4446-AA62-E743F6325049}.Release|x86.Build.0 = Release|x64
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|ARM64.Build.0 = Debug|ARM64
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|x64.ActiveCfg = Debug|x64
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|x64.Build.0 = Debug|x64
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|x86.ActiveCfg = Debug|x64
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|x86.Build.0 = Debug|x64
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|ARM64.ActiveCfg = Release|ARM64
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|ARM64.Build.0 = Release|ARM64
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|x64.ActiveCfg = Release|x64
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|x64.Build.0 = Release|x64
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|x86.ActiveCfg = Release|x64
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|x86.Build.0 = Release|x64
+ {B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|ARM64.Build.0 = Debug|ARM64
+ {B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|x64.ActiveCfg = Debug|x64
+ {B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|x64.Build.0 = Debug|x64
+ {B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|x86.ActiveCfg = Debug|x64
+ {B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|x86.Build.0 = Debug|x64
+ {B41B888C-7DB8-4747-B262-4062E05A230D}.Release|ARM64.ActiveCfg = Release|ARM64
+ {B41B888C-7DB8-4747-B262-4062E05A230D}.Release|ARM64.Build.0 = Release|ARM64
+ {B41B888C-7DB8-4747-B262-4062E05A230D}.Release|x64.ActiveCfg = Release|x64
+ {B41B888C-7DB8-4747-B262-4062E05A230D}.Release|x64.Build.0 = Release|x64
+ {B41B888C-7DB8-4747-B262-4062E05A230D}.Release|x86.ActiveCfg = Release|x64
+ {B41B888C-7DB8-4747-B262-4062E05A230D}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1956,6 +2000,10 @@ Global
{92C39820-9F84-4529-BC7D-22AAE514D63B} = {7AC943C9-52E8-44CF-9083-744D8049667B}
{515554D1-D004-4F7F-A107-2211FC0F6B2C} = {7AC943C9-52E8-44CF-9083-744D8049667B}
{C97D9A5D-206C-454E-997E-009E227D7F02} = {0F14491C-6369-4C45-AAA8-135814E66E6B}
+ {31D1C81D-765F-4446-AA62-E743F6325049} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
+ {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
+ {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
+ {B41B888C-7DB8-4747-B262-4062E05A230D} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index 76d05703f8..f85fd046b5 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -15,6 +15,7 @@
+
@@ -121,13 +122,17 @@
-
+
+
+
+
+
@@ -543,7 +548,14 @@
-
+
+
+
+
+
+
+
+
@@ -1079,6 +1091,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1227,6 +1261,13 @@
+
+
+
+
+
+
+
@@ -1580,7 +1621,7 @@
-
+
@@ -1899,6 +1940,13 @@
+
+
+
+
diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp
index debffba212..b05a4bb216 100644
--- a/installer/PowerToysSetupCustomActions/CustomAction.cpp
+++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp
@@ -1049,7 +1049,7 @@ UINT __stdcall CreateWinAppSDKHardlinksCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
- std::wstring installationFolder, winAppSDKFilesSrcDir, settingsDir, powerRenameDir, measureToolDir;
+ std::wstring installationFolder, winAppSDKFilesSrcDir, settingsDir, powerRenameDir, measureToolDir, hostsFileEditorDir;
hr = WcaInitialize(hInstall, "CreateWinAppSDKHardlinksCA");
ExitOnFailure(hr, "Failed to initialize");
@@ -1058,6 +1058,7 @@ UINT __stdcall CreateWinAppSDKHardlinksCA(MSIHANDLE hInstall)
ExitOnFailure(hr, "Failed to get installation folder");
winAppSDKFilesSrcDir = installationFolder + L"dll\\WinAppSDK\\";
+ hostsFileEditorDir = installationFolder + L"modules\\Hosts\\";
settingsDir = installationFolder + L"Settings\\";
powerRenameDir = installationFolder + L"modules\\PowerRename\\";
measureToolDir = installationFolder + L"modules\\MeasureTool\\";
@@ -1065,6 +1066,7 @@ UINT __stdcall CreateWinAppSDKHardlinksCA(MSIHANDLE hInstall)
for (auto file : winAppSdkFiles)
{
std::error_code ec;
+ std::filesystem::create_hard_link((winAppSDKFilesSrcDir + file).c_str(), (hostsFileEditorDir + file).c_str(), ec);
std::filesystem::create_hard_link((winAppSDKFilesSrcDir + file).c_str(), (settingsDir + file).c_str(), ec);
std::filesystem::create_hard_link((winAppSDKFilesSrcDir + file).c_str(), (powerRenameDir + file).c_str(), ec);
std::filesystem::create_hard_link((winAppSDKFilesSrcDir + file).c_str(), (measureToolDir + file).c_str(), ec);
@@ -1088,7 +1090,7 @@ UINT __stdcall CreatePTInteropHardlinksCA(MSIHANDLE hInstall)
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
std::wstring installationFolder, interopFilesSrcDir, colorPickerDir, powerOCRDir, launcherDir, fancyZonesDir,
- imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir;
+ imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir, hostsFileEditorDir;
hr = WcaInitialize(hInstall, "CreatePTInteropHardlinksCA");
ExitOnFailure(hr, "Failed to initialize");
@@ -1101,6 +1103,7 @@ UINT __stdcall CreatePTInteropHardlinksCA(MSIHANDLE hInstall)
powerOCRDir = installationFolder + L"modules\\PowerOCR\\";
launcherDir = installationFolder + L"modules\\launcher\\";
fancyZonesDir = installationFolder + L"modules\\FancyZones\\";
+ hostsFileEditorDir = installationFolder + L"modules\\Hosts\\";
imageResizerDir = installationFolder + L"modules\\ImageResizer\\";
settingsDir = installationFolder + L"Settings\\";
awakeDir = installationFolder + L"modules\\Awake\\";
@@ -1114,6 +1117,7 @@ UINT __stdcall CreatePTInteropHardlinksCA(MSIHANDLE hInstall)
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (powerOCRDir + file).c_str(), ec);
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (launcherDir + file).c_str(), ec);
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (fancyZonesDir + file).c_str(), ec);
+ std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (hostsFileEditorDir + file).c_str(), ec);
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (imageResizerDir + file).c_str(), ec);
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (settingsDir + file).c_str(), ec);
std::filesystem::create_hard_link((interopFilesSrcDir + file).c_str(), (awakeDir + file).c_str(), ec);
@@ -1138,7 +1142,7 @@ UINT __stdcall DeleteWinAppSDKHardlinksCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
- std::wstring installationFolder, settingsDir, powerRenameDir, measureToolDir;
+ std::wstring installationFolder, settingsDir, powerRenameDir, measureToolDir, hostsFileEditorDir;
hr = WcaInitialize(hInstall, "DeleteWinAppSDKHardlinksCA");
ExitOnFailure(hr, "Failed to initialize");
@@ -1146,6 +1150,7 @@ UINT __stdcall DeleteWinAppSDKHardlinksCA(MSIHANDLE hInstall)
hr = getInstallFolder(hInstall, installationFolder);
ExitOnFailure(hr, "Failed to get installation folder");
+ hostsFileEditorDir = installationFolder + L"modules\\Hosts\\";
settingsDir = installationFolder + L"Settings\\";
powerRenameDir = installationFolder + L"modules\\PowerRename\\";
measureToolDir = installationFolder + L"modules\\MeasureTool\\";
@@ -1154,6 +1159,7 @@ UINT __stdcall DeleteWinAppSDKHardlinksCA(MSIHANDLE hInstall)
{
for (auto file : winAppSdkFiles)
{
+ DeleteFile((hostsFileEditorDir + file).c_str());
DeleteFile((settingsDir + file).c_str());
DeleteFile((powerRenameDir + file).c_str());
DeleteFile((measureToolDir + file).c_str());
@@ -1178,7 +1184,7 @@ UINT __stdcall DeletePTInteropHardlinksCA(MSIHANDLE hInstall)
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
std::wstring installationFolder, interopFilesSrcDir, colorPickerDir, powerOCRDir, launcherDir, fancyZonesDir,
- imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir;
+ imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir, hostsFileEditorDir;
hr = WcaInitialize(hInstall, "DeletePTInteropHardlinksCA");
ExitOnFailure(hr, "Failed to initialize");
@@ -1190,6 +1196,7 @@ UINT __stdcall DeletePTInteropHardlinksCA(MSIHANDLE hInstall)
powerOCRDir = installationFolder + L"modules\\PowerOCR\\";
launcherDir = installationFolder + L"modules\\launcher\\";
fancyZonesDir = installationFolder + L"modules\\FancyZones\\";
+ hostsFileEditorDir = installationFolder + L"modules\\Hosts\\";
imageResizerDir = installationFolder + L"modules\\ImageResizer\\";
settingsDir = installationFolder + L"Settings\\";
awakeDir = installationFolder + L"modules\\Awake\\";
@@ -1204,6 +1211,7 @@ UINT __stdcall DeletePTInteropHardlinksCA(MSIHANDLE hInstall)
DeleteFile((powerOCRDir + file).c_str());
DeleteFile((launcherDir + file).c_str());
DeleteFile((fancyZonesDir + file).c_str());
+ DeleteFile((hostsFileEditorDir + file).c_str());
DeleteFile((imageResizerDir + file).c_str());
DeleteFile((settingsDir + file).c_str());
DeleteFile((awakeDir + file).c_str());
diff --git a/src/common/Common.UI/SettingsDeepLink.cs b/src/common/Common.UI/SettingsDeepLink.cs
index fce6dc0c3a..42da58b8a0 100644
--- a/src/common/Common.UI/SettingsDeepLink.cs
+++ b/src/common/Common.UI/SettingsDeepLink.cs
@@ -23,6 +23,7 @@ namespace Common.UI
FileExplorer,
ShortcutGuide,
VideoConference,
+ Hosts,
}
private static string SettingsWindowNameToString(SettingsWindow value)
@@ -53,6 +54,8 @@ namespace Common.UI
return "ShortcutGuide";
case SettingsWindow.VideoConference:
return "VideoConference";
+ case SettingsWindow.Hosts:
+ return "Hosts";
default:
{
return string.Empty;
diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h
index cc18f04513..d5364d47f4 100644
--- a/src/common/logger/logger_settings.h
+++ b/src/common/logger/logger_settings.h
@@ -35,6 +35,8 @@ struct LogSettings
inline const static std::string alwaysOnTopLoggerName = "always-on-top";
inline const static std::string powerOcrLoggerName = "TextExtractor";
inline const static std::wstring alwaysOnTopLogPath = L"always-on-top-log.txt";
+ inline const static std::string hostsLoggerName = "hosts";
+ inline const static std::wstring hostsLogPath = L"Logs\\hosts-log.txt";
inline const static int retention = 30;
std::wstring logLevel;
LogSettings();
diff --git a/src/modules/Hosts/Hosts.Tests/EntryTest.cs b/src/modules/Hosts/Hosts.Tests/EntryTest.cs
new file mode 100644
index 0000000000..7a61fe147c
--- /dev/null
+++ b/src/modules/Hosts/Hosts.Tests/EntryTest.cs
@@ -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);
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts.Tests/Hosts.Tests.csproj b/src/modules/Hosts/Hosts.Tests/Hosts.Tests.csproj
new file mode 100644
index 0000000000..4611541e11
--- /dev/null
+++ b/src/modules/Hosts/Hosts.Tests/Hosts.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+
+ net6.0-windows10.0.19041.0
+ win10-x64;win10-arm64
+ false
+ false
+ false
+ $(SolutionDir)$(Platform)\$(Configuration)\modules\Hosts\Hosts.Tests\
+ $(SolutionDir)$(Platform)\$(Configuration)\obj\$(AssemblyName)\
+ Hosts.Tests
+ PowerToys.Hosts.Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs
new file mode 100644
index 0000000000..b3d032ca29
--- /dev/null
+++ b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs
@@ -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 _elevationHelper;
+
+ [ClassInitialize]
+ public static void ClassInitialize(TestContext context)
+ {
+ _elevationHelper = new Mock();
+ _elevationHelper.Setup(m => m.IsElevated).Returns(true);
+ }
+
+ [TestMethod]
+ public void Hosts_Exists()
+ {
+ var fileSystem = new MockFileSystem
+ {
+ FileSystemWatcher = new TestFileSystemWatcherFactory(),
+ };
+
+ var userSettings = new Mock();
+
+ 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
+ {
+ })
+ {
+ FileSystemWatcher = new TestFileSystemWatcherFactory(),
+ };
+
+ var userSettings = new Mock();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
+ fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
+
+ await service.WriteAsync(string.Empty, Enumerable.Empty());
+
+ 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();
+ 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();
+ 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);
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts.Tests/TestFileSystemWatcher.cs b/src/modules/Hosts/Hosts.Tests/TestFileSystemWatcher.cs
new file mode 100644
index 0000000000..050c931cfa
--- /dev/null
+++ b/src/modules/Hosts/Hosts.Tests/TestFileSystemWatcher.cs
@@ -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()
+ {
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts.Tests/TestFileSystemWatcherFactory.cs b/src/modules/Hosts/Hosts.Tests/TestFileSystemWatcherFactory.cs
new file mode 100644
index 0000000000..cb62129cfc
--- /dev/null
+++ b/src/modules/Hosts/Hosts.Tests/TestFileSystemWatcherFactory.cs
@@ -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);
+ }
+}
diff --git a/src/modules/Hosts/Hosts/App.xaml b/src/modules/Hosts/Hosts/App.xaml
new file mode 100644
index 0000000000..d4fa24303e
--- /dev/null
+++ b/src/modules/Hosts/Hosts/App.xaml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Hosts/Hosts/App.xaml.cs b/src/modules/Hosts/Hosts/App.xaml.cs
new file mode 100644
index 0000000000..9bd942db23
--- /dev/null
+++ b/src/modules/Hosts/Hosts/App.xaml.cs
@@ -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()
+ 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();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // Views and ViewModels
+ services.AddTransient();
+ services.AddTransient();
+ }).
+ Build();
+
+ UnhandledException += App_UnhandledException;
+
+ new Thread(() =>
+ {
+ // Delete old backups only if running elevated
+ if (!GetService().IsElevated)
+ {
+ return;
+ }
+
+ try
+ {
+ GetService().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);
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Helpers/ElevationHelper.cs b/src/modules/Hosts/Hosts/Helpers/ElevationHelper.cs
new file mode 100644
index 0000000000..306ba02bea
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Helpers/ElevationHelper.cs
@@ -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);
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Helpers/HostsService.cs b/src/modules/Hosts/Hosts/Helpers/HostsService.cs
new file mode 100644
index 0000000000..e7b6eb1ca9
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Helpers/HostsService.cs
@@ -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 Entries)> ReadAsync()
+ {
+ var entries = new List();
+ 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 WriteAsync(string additionalLines, IEnumerable entries)
+ {
+ if (!_elevationHelper.IsElevated)
+ {
+ return false;
+ }
+
+ var lines = new List();
+
+ 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 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;
+ }
+ }
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Helpers/IElevationHelper.cs b/src/modules/Hosts/Hosts/Helpers/IElevationHelper.cs
new file mode 100644
index 0000000000..122949e6a2
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Helpers/IElevationHelper.cs
@@ -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; }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Helpers/IHostsService.cs b/src/modules/Hosts/Hosts/Helpers/IHostsService.cs
new file mode 100644
index 0000000000..06e1c6017a
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Helpers/IHostsService.cs
@@ -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 Entries)> ReadAsync();
+
+ Task WriteAsync(string additionalLines, IEnumerable entries);
+
+ Task PingAsync(string address);
+
+ void CleanupBackup();
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Helpers/Logger.cs b/src/modules/Hosts/Hosts/Helpers/Logger.cs
new file mode 100644
index 0000000000..8a4cd558ce
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Helpers/Logger.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Helpers/NativeMethods.cs b/src/modules/Hosts/Hosts/Helpers/NativeMethods.cs
new file mode 100644
index 0000000000..e35884560c
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Helpers/NativeMethods.cs
@@ -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);
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Helpers/StringHelper.cs b/src/modules/Hosts/Hosts/Helpers/StringHelper.cs
new file mode 100644
index 0000000000..56992d54e5
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Helpers/StringHelper.cs
@@ -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);
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Helpers/ValidationHelper.cs b/src/modules/Hosts/Hosts/Helpers/ValidationHelper.cs
new file mode 100644
index 0000000000..d93c9438ef
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Helpers/ValidationHelper.cs
@@ -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
+ {
+ ///
+ /// Determines whether the address is a valid IPv4
+ ///
+ 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);
+ }
+
+ ///
+ /// Determines whether the address is a valid IPv6
+ ///
+ 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);
+ }
+
+ ///
+ /// Determines whether the hosts are valid
+ ///
+ 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;
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Hosts.csproj b/src/modules/Hosts/Hosts/Hosts.csproj
new file mode 100644
index 0000000000..7a45f9450a
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Hosts.csproj
@@ -0,0 +1,55 @@
+
+
+
+
+ WinExe
+ net6.0-windows10.0.19041.0
+ 10.0.17763.0
+ Hosts
+ app.manifest
+ win10-x64;win10-arm64
+ true
+ true
+ None
+ false
+ false
+ true
+ $(SolutionDir)$(Platform)\$(Configuration)\modules\$(AssemblyName)
+ $(SolutionDir)$(Platform)\$(Configuration)\obj\$(AssemblyName)
+ Hosts
+ PowerToys.Hosts
+ DISABLE_XAML_GENERATED_MAIN,TRACE
+ icon.ico
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+
+
+
diff --git a/src/modules/Hosts/Hosts/MainWindow.xaml b/src/modules/Hosts/Hosts/MainWindow.xaml
new file mode 100644
index 0000000000..e4583f1956
--- /dev/null
+++ b/src/modules/Hosts/Hosts/MainWindow.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Hosts/Hosts/MainWindow.xaml.cs b/src/modules/Hosts/Hosts/MainWindow.xaml.cs
new file mode 100644
index 0000000000..3a1cb13803
--- /dev/null
+++ b/src/modules/Hosts/Hosts/MainWindow.xaml.cs
@@ -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);
+ }
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Models/Entry.cs b/src/modules/Hosts/Hosts/Models/Entry.cs
new file mode 100644
index 0000000000..0c1634ea27
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Models/Entry.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Program.cs b/src/modules/Hosts/Hosts/Program.cs
new file mode 100644
index 0000000000..1754261d51
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Program.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Settings/IUserSettings.cs b/src/modules/Hosts/Hosts/Settings/IUserSettings.cs
new file mode 100644
index 0000000000..5278d60a95
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Settings/IUserSettings.cs
@@ -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; }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Settings/UserSettings.cs b/src/modules/Hosts/Hosts/Settings/UserSettings.cs
new file mode 100644
index 0000000000..26a8e931e0
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Settings/UserSettings.cs
@@ -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(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);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Strings/en-us/Resources.resw b/src/modules/Hosts/Hosts/Strings/en-us/Resources.resw
new file mode 100644
index 0000000000..5a057a99ce
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Strings/en-us/Resources.resw
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Active
+
+
+ Active
+
+
+ Add
+
+
+ New entry
+
+
+ New entry
+
+
+ Additional lines
+
+
+ Additional lines
+
+
+ Cancel
+
+
+ Save
+
+
+ Additional lines
+
+
+ Add new entry
+
+
+ Address
+ "Address" refers to the IP address of the entry
+
+
+ Address
+ "Address" refers to the IP address of the entry
+
+
+ Clear filters
+
+
+ Clear filters
+
+
+ Comment
+ "Comment" refers to the comment of the entry
+
+
+ Comment
+ "Comment" refers to the comment of the entry
+
+
+ Delete
+
+
+ No
+
+
+ Yes
+
+
+ Are you sure you want to delete this entry?
+
+
+ Entries
+
+
+ Cancel
+
+
+ Hosts file has changed by another application. Do you want to reload it?
+ "Hosts" refers to the system hosts file, do not loc
+
+
+ Failed to save hosts file.
+ "Hosts" refers to the system hosts file, do not loc
+
+
+ Filters
+
+
+ Filters
+
+
+ Hosts
+ "Hosts" refers to the system hosts file, do not loc
+
+
+ Seperate multiple hosts by space (e.g. server server.local).
+ Do not localize "server" and "server.local"
+
+
+ Hosts
+ "Hosts" refers to the system hosts file, do not loc
+
+
+ Move down
+
+
+ Move up
+
+
+ Ping
+ "Ping" refers to the command-line utility, do not loc
+
+
+ Reload
+
+
+ Settings
+
+
+ Settings
+
+
+ Update
+
+
+ Update the entry
+
+
+ Accept
+
+
+ Quit
+
+
+ Altering hosts file has direct real world impact of how this computer resolves domain names.
+ "Hosts" refers to the system hosts file, do not loc
+
+
+ Warning
+
+
+ Hosts File Editor
+ "Hosts File Editor" is the name of the utility. "Hosts" refers to the system hosts file, do not loc
+
+
\ No newline at end of file
diff --git a/src/modules/Hosts/Hosts/ViewModels/MainViewModel.cs b/src/modules/Hosts/Hosts/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000000..6b59ab4779
--- /dev/null
+++ b/src/modules/Hosts/Hosts/ViewModels/MainViewModel.cs
@@ -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 _entries;
+
+ public ObservableCollection 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(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(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;
+ }
+ }
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/Views/MainPage.xaml b/src/modules/Hosts/Hosts/Views/MainPage.xaml
new file mode 100644
index 0000000000..7a4a200f42
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Views/MainPage.xaml
@@ -0,0 +1,366 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Hosts/Hosts/Views/MainPage.xaml.cs b/src/modules/Hosts/Hosts/Views/MainPage.xaml.cs
new file mode 100644
index 0000000000..c467511867
--- /dev/null
+++ b/src/modules/Hosts/Hosts/Views/MainPage.xaml.cs
@@ -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();
+ 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();
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts/app.manifest b/src/modules/Hosts/Hosts/app.manifest
new file mode 100644
index 0000000000..87a882d863
--- /dev/null
+++ b/src/modules/Hosts/Hosts/app.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
diff --git a/src/modules/Hosts/Hosts/icon.ico b/src/modules/Hosts/Hosts/icon.ico
new file mode 100644
index 0000000000..6cbba351a1
Binary files /dev/null and b/src/modules/Hosts/Hosts/icon.ico differ
diff --git a/src/modules/Hosts/HostsModuleInterface/HostsModuleInterface.base.rc b/src/modules/Hosts/HostsModuleInterface/HostsModuleInterface.base.rc
new file mode 100644
index 0000000000..5fa3c8b90d
--- /dev/null
+++ b/src/modules/Hosts/HostsModuleInterface/HostsModuleInterface.base.rc
@@ -0,0 +1,40 @@
+#include
+#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
diff --git a/src/modules/Hosts/HostsModuleInterface/HostsModuleInterface.vcxproj b/src/modules/Hosts/HostsModuleInterface/HostsModuleInterface.vcxproj
new file mode 100644
index 0000000000..ab94a58eeb
--- /dev/null
+++ b/src/modules/Hosts/HostsModuleInterface/HostsModuleInterface.vcxproj
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+ 16.0
+ {B41B888C-7DB8-4747-B262-4062E05A230D}
+ Win32Proj
+ HostsModuleInterface
+ HostsModuleInterface
+
+
+
+ DynamicLibrary
+ true
+ v143
+ Unicode
+
+
+ DynamicLibrary
+ false
+ v143
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+ $(SolutionDir)$(Platform)\$(Configuration)\modules\Hosts\
+ PowerToys.HostsModuleInterface
+
+
+ true
+
+
+ false
+
+
+
+ $(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories)
+
+
+
+
+ Use
+ pch.h
+
+
+
+
+
+
+
+
+
+
+
+ Create
+
+
+
+
+
+ {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}
+
+
+ {6955446d-23f7-4023-9bb3-8657f904af99}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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}.
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/Hosts/HostsModuleInterface/HostsModuleInterface.vcxproj.filters b/src/modules/Hosts/HostsModuleInterface/HostsModuleInterface.vcxproj.filters
new file mode 100644
index 0000000000..b824ed2f5d
--- /dev/null
+++ b/src/modules/Hosts/HostsModuleInterface/HostsModuleInterface.vcxproj.filters
@@ -0,0 +1,59 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+ {875a08c6-f610-4667-bd0f-80171ed96072}
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+
+ Resource Files
+
+
+ Resource Files
+
+
+
+
+ Header Files
+
+
+ Generated Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ Resource Files
+
+
+
\ No newline at end of file
diff --git a/src/modules/Hosts/HostsModuleInterface/RCa04224 b/src/modules/Hosts/HostsModuleInterface/RCa04224
new file mode 100644
index 0000000000..561bc1ee9d
Binary files /dev/null and b/src/modules/Hosts/HostsModuleInterface/RCa04224 differ
diff --git a/src/modules/Hosts/HostsModuleInterface/Resource.resx b/src/modules/Hosts/HostsModuleInterface/Resource.resx
new file mode 100644
index 0000000000..d0309bf10f
--- /dev/null
+++ b/src/modules/Hosts/HostsModuleInterface/Resource.resx
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Hosts File Editor
+ "Hosts" refer to the system hosts file, do not loc
+
+
\ No newline at end of file
diff --git a/src/modules/Hosts/HostsModuleInterface/dllmain.cpp b/src/modules/Hosts/HostsModuleInterface/dllmain.cpp
new file mode 100644
index 0000000000..13cade9b38
--- /dev/null
+++ b/src/modules/Hosts/HostsModuleInterface/dllmain.cpp
@@ -0,0 +1,199 @@
+#include "pch.h"
+
+#include "trace.h"
+#include
+#include
+#include
+#include "Generated Files/resource.h"
+
+#include
+#include
+#include
+#include
+#include
+
+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();
+}
\ No newline at end of file
diff --git a/src/modules/Hosts/HostsModuleInterface/packages.config b/src/modules/Hosts/HostsModuleInterface/packages.config
new file mode 100644
index 0000000000..fa024c0634
--- /dev/null
+++ b/src/modules/Hosts/HostsModuleInterface/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/Hosts/HostsModuleInterface/pch.cpp b/src/modules/Hosts/HostsModuleInterface/pch.cpp
new file mode 100644
index 0000000000..1d9f38c57d
--- /dev/null
+++ b/src/modules/Hosts/HostsModuleInterface/pch.cpp
@@ -0,0 +1 @@
+#include "pch.h"
diff --git a/src/modules/Hosts/HostsModuleInterface/pch.h b/src/modules/Hosts/HostsModuleInterface/pch.h
new file mode 100644
index 0000000000..7c6ad2a235
--- /dev/null
+++ b/src/modules/Hosts/HostsModuleInterface/pch.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#define WIN32_LEAN_AND_MEAN
+#include
+#include
\ No newline at end of file
diff --git a/src/modules/Hosts/HostsModuleInterface/resource.base.h b/src/modules/Hosts/HostsModuleInterface/resource.base.h
new file mode 100644
index 0000000000..4c6a75ef09
--- /dev/null
+++ b/src/modules/Hosts/HostsModuleInterface/resource.base.h
@@ -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
+//////////////////////////////
diff --git a/src/modules/Hosts/HostsModuleInterface/trace.cpp b/src/modules/Hosts/HostsModuleInterface/trace.cpp
new file mode 100644
index 0000000000..f09446fe53
--- /dev/null
+++ b/src/modules/Hosts/HostsModuleInterface/trace.cpp
@@ -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);
+}
diff --git a/src/modules/Hosts/HostsModuleInterface/trace.h b/src/modules/Hosts/HostsModuleInterface/trace.h
new file mode 100644
index 0000000000..f6b0ab7388
--- /dev/null
+++ b/src/modules/Hosts/HostsModuleInterface/trace.h
@@ -0,0 +1,8 @@
+#pragma once
+
+class Trace
+{
+public:
+ static void RegisterProvider() noexcept;
+ static void UnregisterProvider() noexcept;
+};
diff --git a/src/runner/main.cpp b/src/runner/main.cpp
index bc6b4484e5..e38bc192e2 100644
--- a/src/runner/main.cpp
+++ b/src/runner/main.cpp
@@ -155,8 +155,8 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
L"modules/MouseUtils/PowerToys.MousePointerCrosshairs.dll",
L"modules/PowerAccent/PowerToys.PowerAccentModuleInterface.dll",
L"modules/PowerOCR/PowerToys.PowerOCRModuleInterface.dll",
-
L"modules/MeasureTool/PowerToys.MeasureToolModuleInterface.dll",
+ L"modules/Hosts/PowerToys.HostsModuleInterface.dll",
};
const auto VCM_PATH = L"modules/VideoConference/PowerToys.VideoConferenceModule.dll";
if (const auto mf = LoadLibraryA("mf.dll"))
diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp
index 656c4e0680..bb613d68bc 100644
--- a/src/runner/settings_window.cpp
+++ b/src/runner/settings_window.cpp
@@ -569,6 +569,8 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value)
return "ShortcutGuide";
case ESettingsWindowNames::VideoConference:
return "VideoConference";
+ case ESettingsWindowNames::Hosts:
+ return "Hosts";
default:
{
Logger::error(L"Can't convert ESettingsWindowNames value={} to string", static_cast(value));
@@ -628,6 +630,10 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
{
return ESettingsWindowNames::VideoConference;
}
+ else if (value == "Hosts")
+ {
+ return ESettingsWindowNames::Hosts;
+ }
else
{
Logger::error(L"Can't convert string value={} to ESettingsWindowNames", winrt::to_hstring(value));
diff --git a/src/runner/settings_window.h b/src/runner/settings_window.h
index 573f5c50e2..7e2f9ed89d 100644
--- a/src/runner/settings_window.h
+++ b/src/runner/settings_window.h
@@ -15,7 +15,8 @@ enum class ESettingsWindowNames
PowerRename,
FileExplorer,
ShortcutGuide,
- VideoConference
+ VideoConference,
+ Hosts
};
std::string ESettingsWindowNames_to_string(ESettingsWindowNames value);
diff --git a/src/settings-ui/Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Settings.UI.Library/EnabledModules.cs
index 3f04c6c6b9..cc34400f2f 100644
--- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs
+++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs
@@ -287,6 +287,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
+ private bool hosts = true;
+
+ [JsonPropertyName("Hosts")]
+ public bool Hosts
+ {
+ get => hosts;
+ set
+ {
+ if (hosts != value)
+ {
+ LogTelemetryEvent(value);
+ hosts = value;
+ }
+ }
+ }
+
public string ToJsonString()
{
return JsonSerializer.Serialize(this);
diff --git a/src/settings-ui/Settings.UI.Library/Enumerations/AdditionalLinesPosition.cs b/src/settings-ui/Settings.UI.Library/Enumerations/AdditionalLinesPosition.cs
new file mode 100644
index 0000000000..72674fc5b6
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/Enumerations/AdditionalLinesPosition.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Settings.UI.Library.Enumerations
+{
+ public enum AdditionalLinesPosition
+ {
+ Top = 0,
+ Bottom = 1,
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HostsProperties.cs b/src/settings-ui/Settings.UI.Library/HostsProperties.cs
new file mode 100644
index 0000000000..b2dae9a5e4
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HostsProperties.cs
@@ -0,0 +1,27 @@
+// 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.Json.Serialization;
+using Settings.UI.Library.Enumerations;
+
+namespace Microsoft.PowerToys.Settings.UI.Library
+{
+ public class HostsProperties
+ {
+ [JsonConverter(typeof(BoolPropertyJsonConverter))]
+ public bool ShowStartupWarning { get; set; }
+
+ [JsonConverter(typeof(BoolPropertyJsonConverter))]
+ public bool LaunchAdministrator { get; set; }
+
+ public AdditionalLinesPosition AdditionalLinesPosition { get; set; }
+
+ public HostsProperties()
+ {
+ ShowStartupWarning = true;
+ LaunchAdministrator = true;
+ AdditionalLinesPosition = AdditionalLinesPosition.Top;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HostsSettings.cs b/src/settings-ui/Settings.UI.Library/HostsSettings.cs
new file mode 100644
index 0000000000..1a28dbcceb
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HostsSettings.cs
@@ -0,0 +1,46 @@
+// 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.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+
+namespace Microsoft.PowerToys.Settings.UI.Library
+{
+ public class HostsSettings : BasePTModuleSettings, ISettingsConfig
+ {
+ public const string ModuleName = "Hosts";
+
+ [JsonPropertyName("properties")]
+ public HostsProperties Properties { get; set; }
+
+ public HostsSettings()
+ {
+ Properties = new HostsProperties();
+ Version = "1.0";
+ Name = ModuleName;
+ }
+
+ public virtual void Save(ISettingsUtils settingsUtils)
+ {
+ // Save settings to file
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ };
+
+ if (settingsUtils == null)
+ {
+ throw new ArgumentNullException(nameof(settingsUtils));
+ }
+
+ settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName);
+ }
+
+ public string GetModuleName() => Name;
+
+ public bool UpgradeSettingsConfiguration() => false;
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/ViewModels/HostsViewModel.cs b/src/settings-ui/Settings.UI.Library/ViewModels/HostsViewModel.cs
new file mode 100644
index 0000000000..3317bc1320
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/ViewModels/HostsViewModel.cs
@@ -0,0 +1,115 @@
+// 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.Runtime.CompilerServices;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
+using Settings.UI.Library.Enumerations;
+
+namespace Settings.UI.Library.ViewModels
+{
+ public class HostsViewModel : Observable
+ {
+ private bool _isElevated;
+
+ private ISettingsUtils SettingsUtils { get; set; }
+
+ private GeneralSettings GeneralSettingsConfig { get; set; }
+
+ private HostsSettings Settings { get; set; }
+
+ private Func SendConfigMSG { get; }
+
+ public ButtonClickCommand LaunchEventHandler => new ButtonClickCommand(Launch);
+
+ public bool IsEnabled
+ {
+ get => GeneralSettingsConfig.Enabled.Hosts;
+
+ set
+ {
+ if (value != GeneralSettingsConfig.Enabled.Hosts)
+ {
+ // Set the status in the general settings configuration
+ GeneralSettingsConfig.Enabled.Hosts = value;
+ OutGoingGeneralSettings snd = new OutGoingGeneralSettings(GeneralSettingsConfig);
+
+ SendConfigMSG(snd.ToString());
+ OnPropertyChanged(nameof(IsEnabled));
+ }
+ }
+ }
+
+ public bool LaunchAdministratorEnabled => IsEnabled && !_isElevated;
+
+ public bool ShowStartupWarning
+ {
+ get => Settings.Properties.ShowStartupWarning;
+ set
+ {
+ if (value != Settings.Properties.ShowStartupWarning)
+ {
+ Settings.Properties.ShowStartupWarning = value;
+ NotifyPropertyChanged();
+ }
+ }
+ }
+
+ public bool LaunchAdministrator
+ {
+ get => Settings.Properties.LaunchAdministrator;
+ set
+ {
+ if (value != Settings.Properties.LaunchAdministrator)
+ {
+ Settings.Properties.LaunchAdministrator = value;
+ NotifyPropertyChanged();
+ }
+ }
+ }
+
+ public int AdditionalLinesPosition
+ {
+ get => (int)Settings.Properties.AdditionalLinesPosition;
+ set
+ {
+ if (value != (int)Settings.Properties.AdditionalLinesPosition)
+ {
+ Settings.Properties.AdditionalLinesPosition = (AdditionalLinesPosition)value;
+ NotifyPropertyChanged();
+ }
+ }
+ }
+
+ public HostsViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, ISettingsRepository moduleSettingsRepository, Func ipcMSGCallBackFunc, bool isElevated)
+ {
+ SettingsUtils = settingsUtils;
+ GeneralSettingsConfig = settingsRepository.SettingsConfig;
+ Settings = moduleSettingsRepository.SettingsConfig;
+ SendConfigMSG = ipcMSGCallBackFunc;
+ _isElevated = isElevated;
+ }
+
+ public void Launch()
+ {
+ var actionName = "Launch";
+
+ if (!_isElevated && LaunchAdministrator)
+ {
+ actionName = "LaunchAdministrator";
+ }
+
+ SendConfigMSG("{\"action\":{\"Hosts\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}");
+ }
+
+ public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ OnPropertyChanged(propertyName);
+ SettingsUtils.SaveSettings(Settings.ToJsonString(), HostsSettings.ModuleName);
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/App.xaml.cs b/src/settings-ui/Settings.UI/App.xaml.cs
index f31728845f..73c8251609 100644
--- a/src/settings-ui/Settings.UI/App.xaml.cs
+++ b/src/settings-ui/Settings.UI/App.xaml.cs
@@ -129,6 +129,7 @@ namespace Microsoft.PowerToys.Settings.UI
case "TextExtractor": StartupPage = typeof(Views.PowerOcrPage); break;
case "VideoConference": StartupPage = typeof(Views.VideoConferencePage); break;
case "MeasureTool": StartupPage = typeof(Views.MeasureToolPage); break;
+ case "Hosts": StartupPage = typeof(Views.HostsPage); break;
default: Debug.Assert(false, "Unexpected SettingsWindow argument value"); break;
}
}
diff --git a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs
index 3bcd711fa4..5039fe9b8c 100644
--- a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs
+++ b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs
@@ -22,6 +22,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
TextExtractor,
VideoConference,
MeasureTool,
+ Hosts,
WhatsNew,
}
}
diff --git a/src/settings-ui/Settings.UI/OOBE/Views/OobeHosts.xaml b/src/settings-ui/Settings.UI/OOBE/Views/OobeHosts.xaml
new file mode 100644
index 0000000000..e3d61c8433
--- /dev/null
+++ b/src/settings-ui/Settings.UI/OOBE/Views/OobeHosts.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/OOBE/Views/OobeHosts.xaml.cs b/src/settings-ui/Settings.UI/OOBE/Views/OobeHosts.xaml.cs
new file mode 100644
index 0000000000..7baee5c877
--- /dev/null
+++ b/src/settings-ui/Settings.UI/OOBE/Views/OobeHosts.xaml.cs
@@ -0,0 +1,58 @@
+// 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 Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
+using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
+using Microsoft.PowerToys.Settings.UI.Views;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Navigation;
+
+namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
+{
+ public sealed partial class OobeHosts : Page
+ {
+ public OobePowerToysModule ViewModel { get; }
+
+ public OobeHosts()
+ {
+ InitializeComponent();
+ ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.Hosts]);
+ DataContext = ViewModel;
+ }
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ ViewModel.LogOpeningModuleEvent();
+ }
+
+ protected override void OnNavigatedFrom(NavigationEventArgs e)
+ {
+ ViewModel.LogClosingModuleEvent();
+ }
+
+ private void Launch_Hosts_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ bool launchAdmin = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.LaunchAdministrator;
+ var actionName = "Launch";
+
+ if (!App.IsElevated && launchAdmin)
+ {
+ actionName = "LaunchAdministrator";
+ }
+
+ ShellPage.SendDefaultIPCMessage("{\"action\":{\"Hosts\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}");
+ }
+
+ private void Launch_Settings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ if (OobeShellPage.OpenMainWindowCallback != null)
+ {
+ OobeShellPage.OpenMainWindowCallback(typeof(HostsPage));
+ }
+
+ ViewModel.LogOpeningSettingsEvent();
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/OOBE/Views/OobeShellPage.xaml b/src/settings-ui/Settings.UI/OOBE/Views/OobeShellPage.xaml
index 74a967ab0d..7243e63dd0 100644
--- a/src/settings-ui/Settings.UI/OOBE/Views/OobeShellPage.xaml
+++ b/src/settings-ui/Settings.UI/OOBE/Views/OobeShellPage.xaml
@@ -75,6 +75,13 @@
ShowAsMonochrome="False" />
+
+
+
+
+
true
+
+
+
+
@@ -68,6 +72,9 @@
Always
+
+ MSBuild:Compile
+
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index 20736c1d74..9b38392a4e 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -2554,4 +2554,72 @@ Activate by holding the key for the character you want to add an accent to, then
Italian
+
+ Quick and simple utility for managing hosts file.
+ "Hosts" refers to the system hosts file, do not loc
+
+
+ Hosts File Editor
+ "Hosts" refers to the system hosts file, do not loc
+
+
+ Hosts File Editor
+ Products name: Navigation view item name for Hosts File Editor
+
+
+ Enable Hosts File Editor
+ "Hosts File Editor" is the name of the utility
+
+
+ Show a warning at startup
+
+
+ Activation
+
+
+ Manage your hosts file
+ "Hosts" refers to the system hosts file, do not loc
+
+
+ Launch Host File Editor
+ "Host File Editor" is a product name
+
+
+ Launch Host File Editor
+ "Host File Editor" is a product name
+
+
+ Additional lines position
+
+
+ Bottom
+
+
+ Top
+
+
+ File
+
+
+ Launch Hosts File Editor
+ "Hosts File Editor" is the name of the utility
+
+
+ Learn more about Hosts File Editor
+ "Hosts File Editor" is the name of the utility
+
+
+ Hosts File Editor is a quick and simple utility for managing hosts file.
+ "Hosts File Editor" is the name of the utility
+
+
+ Hosts File Editor
+ "Hosts File Editor" is the name of the utility
+
+
+ Needs to be launched as administrator in order to make changes to the hosts file
+
+
+ Launch as administrator
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/Views/HostsPage.xaml b/src/settings-ui/Settings.UI/Views/HostsPage.xaml
new file mode 100644
index 0000000000..b53fd2e1af
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Views/HostsPage.xaml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/Views/HostsPage.xaml.cs b/src/settings-ui/Settings.UI/Views/HostsPage.xaml.cs
new file mode 100644
index 0000000000..7ddd63592e
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Views/HostsPage.xaml.cs
@@ -0,0 +1,22 @@
+// 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 Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.UI.Xaml.Controls;
+using Settings.UI.Library.ViewModels;
+
+namespace Microsoft.PowerToys.Settings.UI.Views
+{
+ public sealed partial class HostsPage : Page
+ {
+ private HostsViewModel ViewModel { get; }
+
+ public HostsPage()
+ {
+ InitializeComponent();
+ var settingsUtils = new SettingsUtils();
+ ViewModel = new HostsViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, App.IsElevated);
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Views/ShellPage.xaml b/src/settings-ui/Settings.UI/Views/ShellPage.xaml
index 6637e5f7f7..79379d9081 100644
--- a/src/settings-ui/Settings.UI/Views/ShellPage.xaml
+++ b/src/settings-ui/Settings.UI/Views/ShellPage.xaml
@@ -76,6 +76,12 @@
+
+
+
+
+
+
@@ -137,9 +143,6 @@
-
-
-
diff --git a/tools/BugReportTool/BugReportTool/ProcessesList.cpp b/tools/BugReportTool/BugReportTool/ProcessesList.cpp
index cedab1652b..93a618a393 100644
--- a/tools/BugReportTool/BugReportTool/ProcessesList.cpp
+++ b/tools/BugReportTool/BugReportTool/ProcessesList.cpp
@@ -20,5 +20,6 @@ std::vector processes =
L"PowerToys.ImageResizer.exe",
L"PowerToys.Update.exe",
L"PowerToys.ActionRunner.exe",
- L"PowerToys.AlwaysOnTop.exe"
+ L"PowerToys.AlwaysOnTop.exe",
+ L"PowerToys.Hosts.exe"
};