From b95f9ca7d2b56dffdc9f4df4a1174daa16581e25 Mon Sep 17 00:00:00 2001 From: Ryan Fu <69221034+ryfu-msft@users.noreply.github.com> Date: Fri, 24 Mar 2023 12:55:37 -0700 Subject: [PATCH] Add experimental feature support for enabling Windows Feature dependencies (#3005) --- .github/actions/spelling/expect.txt | 7 + doc/Settings.md | 11 + src/AppInstallerCLICore/Resources.h | 7 + .../Workflows/DependenciesFlow.cpp | 100 +++++++ .../Workflows/DependenciesFlow.h | 6 + .../Workflows/InstallFlow.cpp | 2 + .../FeaturesCommand.cs | 1 + src/AppInstallerCLIE2ETests/InstallCommand.cs | 34 +++ .../TestExeInstaller.WindowsFeature.yaml | 25 ++ .../WinGetSettingsHelper.cs | 1 + .../Shared/Strings/en-us/winget.resw | 30 ++ .../AppInstallerCLITests.vcxproj | 4 + .../AppInstallerCLITests.vcxproj.filters | 6 + .../InstallFlowTest_WindowsFeatures.yaml | 23 ++ src/AppInstallerCLITests/TestHooks.h | 104 +++++++ src/AppInstallerCLITests/WindowsFeature.cpp | 212 +++++++++++++++ .../AppInstallerCommonCore.vcxproj | 2 + .../AppInstallerCommonCore.vcxproj.filters | 6 + .../ExperimentalFeature.cpp | 4 + src/AppInstallerCommonCore/FileLogger.cpp | 26 +- .../Public/AppInstallerFileLogger.h | 2 + .../Public/winget/ExperimentalFeature.h | 1 + .../Public/winget/UserSettings.h | 2 + .../Public/winget/WindowsFeature.h | 210 ++++++++++++++ src/AppInstallerCommonCore/UserSettings.cpp | 1 + src/AppInstallerCommonCore/WindowsFeature.cpp | 256 ++++++++++++++++++ 26 files changed, 1078 insertions(+), 5 deletions(-) create mode 100644 src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.WindowsFeature.yaml create mode 100644 src/AppInstallerCLITests/TestData/InstallFlowTest_WindowsFeatures.yaml create mode 100644 src/AppInstallerCLITests/WindowsFeature.cpp create mode 100644 src/AppInstallerCommonCore/Public/winget/WindowsFeature.h create mode 100644 src/AppInstallerCommonCore/WindowsFeature.cpp diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 0de5a63f31..2643093ebf 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -71,9 +71,11 @@ contactsupport contentfiles contoso contractversion +corecrt count'th countof countryregion +cplusplus createmanifestmetadata cswinrt ctc @@ -84,6 +86,7 @@ deigh deleteifnotneeded desktopappinstaller diskfull +dismapi dllimport DMPAs dnld @@ -231,6 +234,7 @@ mysilentwithprogress nameof nativehandle NESTEDINSTALLER +netfx netlify Newtonsoft NOEXPAND @@ -252,6 +256,7 @@ nuffing objbase objidl ofile +osfhandle Outptr packageinuse packageinusebyapplication @@ -408,7 +413,9 @@ Webserver websites WERSJA wesome +wfopen Whatif +winapifamily windir windowsdeveloper winerror diff --git a/doc/Settings.md b/doc/Settings.md index f6a5925392..afc5d32d74 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -274,3 +274,14 @@ You can enable the feature as shown below. "configuration": true }, ``` + +### windowsFeature + +This feature enables the ability to enable Windows Feature dependencies during installation. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "windowsFeature": true + }, +``` \ No newline at end of file diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 1a2e4f7b83..021c50398c 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -98,6 +98,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(Done); WINGET_DEFINE_RESOURCE_STRINGID(Downloading); WINGET_DEFINE_RESOURCE_STRINGID(EnableAdminSettingFailed); + WINGET_DEFINE_RESOURCE_STRINGID(EnablingWindowsFeature); WINGET_DEFINE_RESOURCE_STRINGID(ExactArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalCommandLongDescription); @@ -112,6 +113,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ExtractArchiveSucceeded); WINGET_DEFINE_RESOURCE_STRINGID(ExtractingArchive); WINGET_DEFINE_RESOURCE_STRINGID(ExtraPositionalError); + WINGET_DEFINE_RESOURCE_STRINGID(FailedToEnableWindowsFeature); + WINGET_DEFINE_RESOURCE_STRINGID(FailedToEnableWindowsFeatureOverridden); + WINGET_DEFINE_RESOURCE_STRINGID(FailedToEnableWindowsFeatureOverrideRequired); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledByAdminSettingMessage); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(FeaturesCommandLongDescription); @@ -322,6 +326,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PurgeInstallDirectory); WINGET_DEFINE_RESOURCE_STRINGID(QueryArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(RainbowArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(RebootRequiredToEnableWindowsFeatureOverridden); + WINGET_DEFINE_RESOURCE_STRINGID(RebootRequiredToEnableWindowsFeatureOverrideRequired); WINGET_DEFINE_RESOURCE_STRINGID(RelatedLink); WINGET_DEFINE_RESOURCE_STRINGID(RenameArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ReparsePointsNotSupportedError); @@ -494,6 +500,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(VersionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(VersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(WaitArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(WindowsFeatureNotFound); WINGET_DEFINE_RESOURCE_STRINGID(WindowsFeaturesDependencies); WINGET_DEFINE_RESOURCE_STRINGID(WindowsLibrariesDependencies); WINGET_DEFINE_RESOURCE_STRINGID(WindowsStoreTerms); diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp index 2acc0a40a3..e86788d156 100644 --- a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp @@ -6,10 +6,12 @@ #include "ManifestComparator.h" #include "InstallFlow.h" #include "winget\DependenciesGraph.h" +#include "winget\WindowsFeature.h" #include "DependencyNodeProcessor.h" using namespace AppInstaller::Repository; using namespace AppInstaller::Manifest; +using namespace AppInstaller::WindowsFeature; namespace AppInstaller::CLI::Workflow { @@ -133,6 +135,104 @@ namespace AppInstaller::CLI::Workflow } } + void EnableWindowsFeaturesDependencies(Execution::Context& context) + { + if (!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::WindowsFeature)) + { + return; + } + + const auto& rootDependencies = context.Get()->Dependencies; + + if (rootDependencies.Empty()) + { + return; + } + + if (rootDependencies.HasAnyOf(DependencyType::WindowsFeature)) + { + context << Workflow::EnsureRunningAsAdmin; + if (context.IsTerminated()) + { + return; + } + + HRESULT hr = S_OK; + std::shared_ptr dismHelper = std::make_shared(); + + bool force = context.Args.Contains(Execution::Args::Type::Force); + bool rebootRequired = false; + + rootDependencies.ApplyToType(DependencyType::WindowsFeature, [&context, &hr, &dismHelper, &force, &rebootRequired](Dependency dependency) + { + if (SUCCEEDED(hr) || force) + { + auto featureName = dependency.Id(); + WindowsFeature::WindowsFeature windowsFeature = dismHelper->GetWindowsFeature(featureName); + + if (windowsFeature.DoesExist()) + { + if (!windowsFeature.IsEnabled()) + { + Utility::LocIndString featureDisplayName = windowsFeature.GetDisplayName(); + Utility::LocIndView locIndFeatureName{ featureName }; + + context.Reporter.Info() << Resource::String::EnablingWindowsFeature(featureDisplayName, locIndFeatureName) << std::endl; + + AICLI_LOG(Core, Info, << "Enabling Windows Feature [" << featureName << "] returned with HRESULT: " << hr); + auto enableFeatureFunction = [&](IProgressCallback& progress)->HRESULT { return windowsFeature.Enable(progress); }; + hr = context.Reporter.ExecuteWithProgress(enableFeatureFunction, true); + + if (FAILED(hr)) + { + AICLI_LOG(Core, Error, << "Failed to enable Windows Feature " << featureDisplayName << " [" << locIndFeatureName << "] with exit code: " << hr); + context.Reporter.Warn() << Resource::String::FailedToEnableWindowsFeature(featureDisplayName, locIndFeatureName) << std::endl + << GetUserPresentableMessage(hr) << std::endl; + } + + if (hr == ERROR_SUCCESS_REBOOT_REQUIRED || windowsFeature.GetRestartRequiredStatus() == DismRestartType::DismRestartRequired) + { + rebootRequired = true; + } + } + } + else + { + // Note: If a feature is not found, continue enabling the rest of the dependencies but block immediately after unless force arg is present. + AICLI_LOG(Core, Info, << "Windows Feature [" << featureName << "] does not exist"); + hr = APPINSTALLER_CLI_ERROR_INSTALL_MISSING_DEPENDENCY; + context.Reporter.Warn() << Resource::String::WindowsFeatureNotFound(Utility::LocIndView{ featureName }) << std::endl; + } + } + }); + + if (FAILED(hr)) + { + if (force) + { + context.Reporter.Warn() << Resource::String::FailedToEnableWindowsFeatureOverridden << std::endl; + } + else + { + context.Reporter.Error() << Resource::String::FailedToEnableWindowsFeatureOverrideRequired << std::endl; + AICLI_TERMINATE_CONTEXT(hr); + } + } + else if (rebootRequired) + { + if (force) + { + context.Reporter.Warn() << Resource::String::RebootRequiredToEnableWindowsFeatureOverridden << std::endl; + } + else + { + context.Reporter.Error() << Resource::String::RebootRequiredToEnableWindowsFeatureOverrideRequired << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_TO_INSTALL); + } + } + } + } + void ManagePackageDependencies::operator()(Execution::Context& context) const { if (!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Dependencies)) diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.h b/src/AppInstallerCLICore/Workflows/DependenciesFlow.h index f6ffa00a19..7e99d4a6f8 100644 --- a/src/AppInstallerCLICore/Workflows/DependenciesFlow.h +++ b/src/AppInstallerCLICore/Workflows/DependenciesFlow.h @@ -59,4 +59,10 @@ namespace AppInstaller::CLI::Workflow // Inputs: PackageVersion, Manifest // Outputs: DependencySource void OpenDependencySource(Execution::Context& context); + + // Enables the Windows Feature dependencies. + // Required Args: None + // Inputs: Manifest, Installer + // Outputs: None + void EnableWindowsFeaturesDependencies(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index 6a1ce0d66f..9dd3778e6b 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -489,11 +489,13 @@ namespace AppInstaller::CLI::Workflow void DownloadSinglePackage(Execution::Context& context) { + // TODO: Split dependencies from download flow to prevent multiple installations. context << Workflow::ReportIdentityAndInstallationDisclaimer << Workflow::ShowPromptsForSinglePackage(/* ensureAcceptance */ true) << Workflow::GetDependenciesFromInstaller << Workflow::ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << + Workflow::EnableWindowsFeaturesDependencies << Workflow::ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << Workflow::DownloadInstaller; } diff --git a/src/AppInstallerCLIE2ETests/FeaturesCommand.cs b/src/AppInstallerCLIE2ETests/FeaturesCommand.cs index a729ce33d8..bb581138ac 100644 --- a/src/AppInstallerCLIE2ETests/FeaturesCommand.cs +++ b/src/AppInstallerCLIE2ETests/FeaturesCommand.cs @@ -53,6 +53,7 @@ public void EnableExperimentalFeatures() WinGetSettingsHelper.ConfigureFeature("experimentalCmd", true); WinGetSettingsHelper.ConfigureFeature("directMSI", true); WinGetSettingsHelper.ConfigureFeature("uninstallPreviousArgument", true); + WinGetSettingsHelper.ConfigureFeature("windowsFeature", true); var result = TestCommon.RunAICLICommand("features", string.Empty); Assert.True(result.StdOut.Contains("Enabled")); } diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index e19b99ba43..a17ff1f2b9 100644 --- a/src/AppInstallerCLIE2ETests/InstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -14,6 +14,15 @@ namespace AppInstallerCLIE2ETests /// public class InstallCommand : BaseCommand { + /// + /// One time setup. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("windowsFeature", true); + } + /// /// Set up. /// @@ -613,5 +622,30 @@ public void InstallExeWithLatestInstalledWithForce() Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(baseDir)); Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/execustom")); } + + /// + /// Test install a package with an invalid Windows Feature dependency. + /// + [Test] + public void InstallWithWindowsFeatureDependency_FeatureNotFound() + { + var testDir = TestCommon.GetRandomTestDir(); + var installResult = TestCommon.RunAICLICommand("install", $"AppInstallerTest.WindowsFeature -l {testDir}"); + Assert.AreEqual(Constants.ErrorCode.ERROR_INSTALL_MISSING_DEPENDENCY, installResult.ExitCode); + Assert.True(installResult.StdOut.Contains("The feature [invalidFeature] was not found.")); + } + + /// + /// Test install a package with a Windows Feature dependency using the force argument. + /// + [Test] + public void InstallWithWindowsFeatureDependency_Force() + { + var testDir = TestCommon.GetRandomTestDir(); + var installResult = TestCommon.RunAICLICommand("install", $"AppInstallerTest.WindowsFeature --silent --force -l {testDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, installResult.ExitCode); + Assert.True(installResult.StdOut.Contains("Successfully installed")); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(testDir)); + } } } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.WindowsFeature.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.WindowsFeature.yaml new file mode 100644 index 0000000000..e0245698b0 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.WindowsFeature.yaml @@ -0,0 +1,25 @@ +PackageIdentifier: AppInstallerTest.WindowsFeature +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: TestWindowsFeature +ShortDescription: Installs exe installer with valid Windows Features dependencies +Publisher: Microsoft Corporation +License: Test +Installers: + - Architecture: x64 + InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + InstallerType: exe + InstallerSha256: + InstallerSwitches: + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /LogFile + InstallLocation: /InstallDir + Dependencies: + WindowsFeatures: + - netfx3 + - invalidFeature +ManifestType: singleton +ManifestVersion: 1.4.0 \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs index 2352addb04..6834550c57 100644 --- a/src/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs +++ b/src/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs @@ -191,6 +191,7 @@ public static void InitializeAllFeatures(bool status) ConfigureFeature("directMSI", status); ConfigureFeature("pinning", status); ConfigureFeature("configuration", status); + ConfigureFeature("windowsFeature", status); } } } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 22a53a9277..b6a91c9bf6 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1791,4 +1791,34 @@ Please specify one of them using the --source option to proceed. File not found. + + The feature [{0}] was not found. + {Locked="{0}"} Error message displayed when a Windows feature was not found on the system. + + + Failed to enable Windows Feature dependencies. To proceed with installation, use '--force'. + {Locked="--force"} + + + Reboot required to fully enable the Windows Feature(s); to override this check use '--force'. + "Windows Feature(s)" is the name of the options Windows features setting. + + + Failed to enable Windows Feature dependencies; proceeding due to --force + "Windows Feature(s)" is the name of the options Windows features setting. +{Locked="--force"} + + + Enabling {0} [{1}]... + {Locked="{0}","{1}"} Message displayed to the user regarding which Windows Feature is being enabled. + + + Failed to enable {0} [{1}] feature. + {Locked="{0}"} Windows Feature display name +{Locked="{1}"} Windows Feature name + + + Reboot required to fully enable the Windows Feature(s); proceeding due to --force + "Windows Feature(s)" is the name of the options Windows features setting. + \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 766336708f..f95bf0c608 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -250,6 +250,7 @@ + @@ -329,6 +330,9 @@ true + + true + true diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 857675b614..cf163e5dc5 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -290,6 +290,9 @@ Source Files + + Source Files\CLI + @@ -612,6 +615,9 @@ TestData + + TestData + TestData diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_WindowsFeatures.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_WindowsFeatures.yaml new file mode 100644 index 0000000000..e07b2da908 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/InstallFlowTest_WindowsFeatures.yaml @@ -0,0 +1,23 @@ +PackageIdentifier: AppInstallerCliTest.WindowsFeatures +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Installer +ShortDescription: Installs exe installer with valid Windows Features dependencies +Publisher: Microsoft Corporation +License: Test +Installers: + - Architecture: x86 + InstallerUrl: https://ThisIsNotUsed + InstallerType: exe + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + InstallerSwitches: + Custom: /custom /scope=machine + SilentWithProgress: /silentwithprogress + Silent: /silence + Update: /update + Dependencies: + WindowsFeatures: + - testFeature1 + - testFeature2 +ManifestType: singleton +ManifestVersion: 1.4.0 \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h index 0496d4a20f..9f733137e5 100644 --- a/src/AppInstallerCLITests/TestHooks.h +++ b/src/AppInstallerCLITests/TestHooks.h @@ -11,6 +11,7 @@ #include #include #include +#include #ifdef AICLI_DISABLE_TEST_HOOKS static_assert(false, "Test hooks have been disabled"); @@ -58,6 +59,16 @@ namespace AppInstaller { void TestHook_SetScanArchiveResult_Override(bool* status); } + + namespace WindowsFeature + { + void TestHook_MockDismHelper_Override(bool status); + void TestHook_SetEnableWindowsFeatureResult_Override(HRESULT* result); + void TestHook_SetIsWindowsFeatureEnabledResult_Override(bool* status); + void TestHook_SetDoesWindowsFeatureExistResult_Override(bool* status); + void TestHook_SetWindowsFeatureGetDisplayNameResult_Override(Utility::LocIndString* displayName); + void TestHook_SetWindowsFeatureGetRestartStatusResult_Override(AppInstaller::WindowsFeature::DismRestartType* restartType); + } } namespace TestHook @@ -106,4 +117,97 @@ namespace TestHook AppInstaller::Repository::Microsoft::TestHook_SetPinningIndex_Override({}); } }; + + struct MockDismHelper_Override + { + MockDismHelper_Override() + { + AppInstaller::WindowsFeature::TestHook_MockDismHelper_Override(true); + } + + ~MockDismHelper_Override() + { + AppInstaller::WindowsFeature::TestHook_MockDismHelper_Override(false); + } + }; + + struct SetEnableWindowsFeatureResult_Override + { + SetEnableWindowsFeatureResult_Override(HRESULT result) : m_result(result) + { + AppInstaller::WindowsFeature::TestHook_SetEnableWindowsFeatureResult_Override(&m_result); + } + + ~SetEnableWindowsFeatureResult_Override() + { + AppInstaller::WindowsFeature::TestHook_SetEnableWindowsFeatureResult_Override(nullptr); + } + + private: + HRESULT m_result; + }; + + struct SetIsWindowsFeatureEnabledResult_Override + { + SetIsWindowsFeatureEnabledResult_Override(bool status) : m_status(status) + { + AppInstaller::WindowsFeature::TestHook_SetIsWindowsFeatureEnabledResult_Override(&m_status); + } + + ~SetIsWindowsFeatureEnabledResult_Override() + { + AppInstaller::WindowsFeature::TestHook_SetIsWindowsFeatureEnabledResult_Override(nullptr); + } + + private: + bool m_status; + }; + + struct SetDoesWindowsFeatureExistResult_Override + { + SetDoesWindowsFeatureExistResult_Override(bool status) : m_status(status) + { + AppInstaller::WindowsFeature::TestHook_SetDoesWindowsFeatureExistResult_Override(&m_status); + } + + ~SetDoesWindowsFeatureExistResult_Override() + { + AppInstaller::WindowsFeature::TestHook_SetDoesWindowsFeatureExistResult_Override(nullptr); + } + + private: + bool m_status; + }; + + struct SetWindowsFeatureGetDisplayNameResult_Override + { + SetWindowsFeatureGetDisplayNameResult_Override(AppInstaller::Utility::LocIndString displayName) : m_displayName(displayName) + { + AppInstaller::WindowsFeature::TestHook_SetWindowsFeatureGetDisplayNameResult_Override(&m_displayName); + } + + ~SetWindowsFeatureGetDisplayNameResult_Override() + { + AppInstaller::WindowsFeature::TestHook_SetWindowsFeatureGetDisplayNameResult_Override(nullptr); + } + + private: + AppInstaller::Utility::LocIndString m_displayName; + }; + + struct SetWindowsFeatureGetRestartStatusResult_Override + { + SetWindowsFeatureGetRestartStatusResult_Override(AppInstaller::WindowsFeature::DismRestartType restartType) : m_restartType(restartType) + { + AppInstaller::WindowsFeature::TestHook_SetWindowsFeatureGetRestartStatusResult_Override(&m_restartType); + } + + ~SetWindowsFeatureGetRestartStatusResult_Override() + { + AppInstaller::WindowsFeature::TestHook_SetWindowsFeatureGetRestartStatusResult_Override(nullptr); + } + + private: + AppInstaller::WindowsFeature::DismRestartType m_restartType; + }; } \ No newline at end of file diff --git a/src/AppInstallerCLITests/WindowsFeature.cpp b/src/AppInstallerCLITests/WindowsFeature.cpp new file mode 100644 index 0000000000..0f00b55658 --- /dev/null +++ b/src/AppInstallerCLITests/WindowsFeature.cpp @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "WorkflowCommon.h" +#include +#include +#include +#include "TestHooks.h" + +using namespace AppInstaller::CLI; +using namespace AppInstaller::Settings; +using namespace AppInstaller::Utility; +using namespace AppInstaller::WindowsFeature; +using namespace TestCommon; + +TEST_CASE("InstallFlow_WindowsFeatureDoesNotExist", "[windowsFeature]") +{ + if (!AppInstaller::Runtime::IsRunningAsAdmin()) + { + WARN("Test requires admin privilege. Skipped."); + return; + } + + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + TestCommon::TestUserSettings testSettings; + testSettings.Set(true); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_WindowsFeatures.yaml").GetPath().u8string()); + + auto mockDismHelperOverride = TestHook::MockDismHelper_Override(); + auto doesFeatureExistOverride = TestHook::SetDoesWindowsFeatureExistResult_Override(false); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_INSTALL_MISSING_DEPENDENCY); + REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::WindowsFeatureNotFound(LocIndView{ "testFeature1" })).get()) != std::string::npos); + + // "badFeature" should not be displayed as the flow should terminate after failing to find the first feature. + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::WindowsFeatureNotFound(LocIndView{ "testFeature2" })).get()) == std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::FailedToEnableWindowsFeatureOverrideRequired).get()) != std::string::npos); +} + +TEST_CASE("InstallFlow_FailedToEnableWindowsFeature", "[windowsFeature]") +{ + if (!AppInstaller::Runtime::IsRunningAsAdmin()) + { + WARN("Test requires admin privilege. Skipped."); + return; + } + + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + TestCommon::TestUserSettings testSettings; + testSettings.Set(true); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_WindowsFeatures.yaml").GetPath().u8string()); + + // Override with arbitrary DISM api error (DISMAPI_E_DISMAPI_NOT_INITIALIZED) and make windows feature discoverable. + HRESULT dismErrorResult = 0xc0040001; + auto mockDismHelperOverride = TestHook::MockDismHelper_Override(); + auto doesFeatureExistOverride = TestHook::SetDoesWindowsFeatureExistResult_Override(true); + auto setIsFeatureEnabledOverride = TestHook::SetIsWindowsFeatureEnabledResult_Override(false); + auto setEnableFeatureOverride = TestHook::SetEnableWindowsFeatureResult_Override(dismErrorResult); + auto getDisplayNameOverride = TestHook::SetWindowsFeatureGetDisplayNameResult_Override(LocIndString{ "Test Windows Feature"_liv }); + auto getRestartStatusOverride = TestHook::SetWindowsFeatureGetRestartStatusResult_Override(DismRestartNo); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + REQUIRE(context.GetTerminationHR() == dismErrorResult); + REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::FailedToEnableWindowsFeatureOverrideRequired).get()) != std::string::npos); +} + +TEST_CASE("InstallFlow_FailedToEnableWindowsFeature_Force", "[windowsFeature]") +{ + if (!AppInstaller::Runtime::IsRunningAsAdmin()) + { + WARN("Test requires admin privilege. Skipped."); + return; + } + + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + TestCommon::TestUserSettings testSettings; + testSettings.Set(true); + + // Override with arbitrary DISM api error (DISMAPI_E_DISMAPI_NOT_INITIALIZED) and make windows feature discoverable. + HRESULT dismErrorResult = 0xc0040001; + LocIndString testFeatureDisplayName = LocIndString{ "Test Windows Feature"_liv }; + auto mockDismHelperOverride = TestHook::MockDismHelper_Override(); + auto setEnableFeatureOverride = TestHook::SetEnableWindowsFeatureResult_Override(dismErrorResult); + auto doesFeatureExistOverride = TestHook::SetDoesWindowsFeatureExistResult_Override(true); + auto setIsFeatureEnabledOverride = TestHook::SetIsWindowsFeatureEnabledResult_Override(false); + auto getDisplayNameOverride = TestHook::SetWindowsFeatureGetDisplayNameResult_Override(testFeatureDisplayName); + auto getRestartStatusOverride = TestHook::SetWindowsFeatureGetRestartStatusResult_Override(DismRestartNo); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_WindowsFeatures.yaml").GetPath().u8string()); + context.Args.AddArg(Execution::Args::Type::Force); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify Installer is called and parameters are passed in. + REQUIRE(context.GetTerminationHR() == ERROR_SUCCESS); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::FailedToEnableWindowsFeature(testFeatureDisplayName, LocIndView{ "testFeature1" })).get()) != std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::FailedToEnableWindowsFeature(testFeatureDisplayName, LocIndView{ "testFeature2" })).get()) != std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::FailedToEnableWindowsFeatureOverridden).get()) != std::string::npos); + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + std::ifstream installResultFile(installResultPath.GetPath()); + REQUIRE(installResultFile.is_open()); + std::string installResultStr; + std::getline(installResultFile, installResultStr); + REQUIRE(installResultStr.find("/custom") != std::string::npos); + REQUIRE(installResultStr.find("/silentwithprogress") != std::string::npos); +} + +TEST_CASE("InstallFlow_RebootRequired", "[windowsFeature]") +{ + if (!AppInstaller::Runtime::IsRunningAsAdmin()) + { + WARN("Test requires admin privilege. Skipped."); + return; + } + + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + TestCommon::TestUserSettings testSettings; + testSettings.Set(true); + + // Override with reboot required HRESULT. + auto mockDismHelperOverride = TestHook::MockDismHelper_Override(); + auto setEnableFeatureOverride = TestHook::SetEnableWindowsFeatureResult_Override(ERROR_SUCCESS_REBOOT_REQUIRED); + auto setIsFeatureEnabledOverride = TestHook::SetIsWindowsFeatureEnabledResult_Override (false); + auto doesFeatureExistOverride = TestHook::SetDoesWindowsFeatureExistResult_Override(true); + auto getDisplayNameOverride = TestHook::SetWindowsFeatureGetDisplayNameResult_Override(LocIndString{ "Test Windows Feature"_liv }); + auto getRestartStatusOverride = TestHook::SetWindowsFeatureGetRestartStatusResult_Override(DismRestartRequired); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_WindowsFeatures.yaml").GetPath().u8string()); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_TO_INSTALL); + REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::RebootRequiredToEnableWindowsFeatureOverrideRequired).get()) != std::string::npos); +} + +TEST_CASE("InstallFlow_RebootRequired_Force", "[windowsFeature]") +{ + if (!AppInstaller::Runtime::IsRunningAsAdmin()) + { + WARN("Test requires admin privilege. Skipped."); + return; + } + + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + TestCommon::TestUserSettings testSettings; + testSettings.Set(true); + + // Override with reboot required HRESULT. + auto mockDismHelperOverride = TestHook::MockDismHelper_Override(); + auto setEnableFeatureOverride = TestHook::SetEnableWindowsFeatureResult_Override(ERROR_SUCCESS_REBOOT_REQUIRED); + auto setIsFeatureEnabledOverride = TestHook::SetIsWindowsFeatureEnabledResult_Override(false); + auto doesFeatureExistOverride = TestHook::SetDoesWindowsFeatureExistResult_Override(true); + auto getDisplayNameOverride = TestHook::SetWindowsFeatureGetDisplayNameResult_Override(LocIndString{ "Test Windows Feature"_liv }); + auto getRestartStatusOverride = TestHook::SetWindowsFeatureGetRestartStatusResult_Override(DismRestartRequired); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_WindowsFeatures.yaml").GetPath().u8string()); + context.Args.AddArg(Execution::Args::Type::Force); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify Installer is called and parameters are passed in. + REQUIRE(context.GetTerminationHR() == ERROR_SUCCESS); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::RebootRequiredToEnableWindowsFeatureOverridden).get()) != std::string::npos); + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + std::ifstream installResultFile(installResultPath.GetPath()); + REQUIRE(installResultFile.is_open()); + std::string installResultStr; + std::getline(installResultFile, installResultStr); + REQUIRE(installResultStr.find("/custom") != std::string::npos); + REQUIRE(installResultStr.find("/silentwithprogress") != std::string::npos); +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj index ac786715f2..3aac7b1ff5 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -328,6 +328,7 @@ + @@ -397,6 +398,7 @@ + diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters index ca0cef8615..1ac42c4677 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -189,6 +189,9 @@ Public\winget + + Public\winget + @@ -335,6 +338,9 @@ Source Files + + Source Files + diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index 67e5e51b46..0718cc107d 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -48,6 +48,8 @@ namespace AppInstaller::Settings return userSettings.Get(); case ExperimentalFeature::Feature::Configuration: return userSettings.Get(); + case ExperimentalFeature::Feature::WindowsFeature: + return userSettings.Get(); default: THROW_HR(E_UNEXPECTED); } @@ -85,6 +87,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Uninstall Previous Argument", "uninstallPreviousArgument", "https://aka.ms/winget-settings", Feature::UninstallPreviousArgument }; case Feature::Configuration: return ExperimentalFeature{ "Configuration", "configuration", "https://aka.ms/winget-settings#configuration", Feature::Configuration }; + case Feature::WindowsFeature: + return ExperimentalFeature{ "Windows Feature Dependencies", "windowsFeature", "https://aka.ms/winget-settings", Feature::WindowsFeature }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/FileLogger.cpp b/src/AppInstallerCommonCore/FileLogger.cpp index 45311b6eb2..dc3265b239 100644 --- a/src/AppInstallerCommonCore/FileLogger.cpp +++ b/src/AppInstallerCommonCore/FileLogger.cpp @@ -6,6 +6,7 @@ #include "Public/AppInstallerRuntime.h" #include "Public/AppInstallerStrings.h" #include "Public/AppInstallerDateTime.h" +#include namespace AppInstaller::Logging @@ -15,7 +16,6 @@ namespace AppInstaller::Logging static constexpr std::string_view s_fileLoggerDefaultFilePrefix = "WinGet"sv; static constexpr std::string_view s_fileLoggerDefaultFileExt = ".log"sv; - static constexpr std::ios_base::openmode s_fileLoggerDefaultOpenMode = std::ios_base::out | std::ios_base::app; FileLogger::FileLogger() : FileLogger(s_fileLoggerDefaultFilePrefix) {} @@ -23,8 +23,7 @@ namespace AppInstaller::Logging { m_name = GetNameForPath(filePath); m_filePath = filePath; - - m_stream.open(m_filePath, s_fileLoggerDefaultOpenMode); + OpenFileLoggerStream(); } FileLogger::FileLogger(const std::string_view fileNamePrefix) @@ -32,8 +31,7 @@ namespace AppInstaller::Logging m_name = "file"; m_filePath = Runtime::GetPathTo(Runtime::PathName::DefaultLogLocation); m_filePath /= fileNamePrefix.data() + ('-' + Utility::GetCurrentTimeForFilename() + s_fileLoggerDefaultFileExt.data()); - - m_stream.open(m_filePath, s_fileLoggerDefaultOpenMode); + OpenFileLoggerStream(); } FileLogger::~FileLogger() @@ -125,4 +123,22 @@ namespace AppInstaller::Logging catch (...) {} }).detach(); } + + void FileLogger::OpenFileLoggerStream() + { + // Prevent inheritance to ensure log file handle is not opened by other processes. + FILE* filePtr; + errno_t fopenError = _wfopen_s(&filePtr, m_filePath.wstring().c_str(), L"w"); + if (!fopenError) + { + THROW_HR_IF(E_UNEXPECTED, filePtr == nullptr); + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(reinterpret_cast(_get_osfhandle(_fileno(filePtr))), HANDLE_FLAG_INHERIT, 0)); + m_stream = std::ofstream{ filePtr }; + } + else + { + AICLI_LOG(Core, Error, << "Failed to open log file " << m_filePath.u8string()); + throw std::system_error(fopenError, std::generic_category()); + } + } } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerFileLogger.h b/src/AppInstallerCommonCore/Public/AppInstallerFileLogger.h index 843eb8beaf..c506eccab0 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerFileLogger.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerFileLogger.h @@ -50,5 +50,7 @@ namespace AppInstaller::Logging std::string m_name; std::filesystem::path m_filePath; std::ofstream m_stream; + + void OpenFileLoggerStream(); }; } diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index 5e031f838e..ad0a2dd5d6 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -27,6 +27,7 @@ namespace AppInstaller::Settings Pinning = 0x4, UninstallPreviousArgument = 0x8, Configuration = 0x10, + WindowsFeature = 0x20, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index ef4bd4ffa1..ffb5988597 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -73,6 +73,7 @@ namespace AppInstaller::Settings EFPinning, EFUninstallPreviousArgument, EFConfiguration, + EFWindowsFeature, // Telemetry TelemetryDisable, // Install behavior @@ -144,6 +145,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFPinning, bool, bool, false, ".experimentalFeatures.pinning"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFUninstallPreviousArgument, bool, bool, false, ".experimentalFeatures.uninstallPreviousArgument"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFConfiguration, bool, bool, false, ".experimentalFeatures.configuration"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFWindowsFeature, bool, bool, false, ".experimentalFeatures.windowsFeature"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/Public/winget/WindowsFeature.h b/src/AppInstallerCommonCore/Public/winget/WindowsFeature.h new file mode 100644 index 0000000000..a2ec6c09dc --- /dev/null +++ b/src/AppInstallerCommonCore/Public/winget/WindowsFeature.h @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include + +namespace AppInstaller::WindowsFeature +{ + /****************************************************************************\ + + Declaration copied from DismApi.h to support enabling Windows Features. + + Copyright (c) Microsoft Corporation. + All rights reserved. + + \****************************************************************************/ + +#ifndef _DISMAPI_H_ +#define _DISMAPI_H_ + +#include + +#pragma region Desktop Family or DISM Package +#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP | WINAPI_PARTITION_PKG_DISM) + +#ifdef __cplusplus + extern "C" + { +#endif + + typedef UINT DismSession; + typedef void(CALLBACK* DISM_PROGRESS_CALLBACK)(_In_ UINT Current, _In_ UINT Total, _In_opt_ PVOID UserData); + +#define DISM_ONLINE_IMAGE L"DISM_{53BFAE52-B167-4E2F-A258-0A37B57FF845}" +#define DISM_SESSION_DEFAULT 0 + + typedef enum _DismLogLevel + { + DismLogErrors = 0, + DismLogErrorsWarnings, + DismLogErrorsWarningsInfo, + DismLogErrorsWarningsInfoDebug + } DismLogLevel; + + typedef enum _DismPackageIdentifier + { + DismPackageNone = 0, + DismPackageName, + DismPackagePath + } DismPackageIdentifier; + + typedef enum _DismPackageFeatureState + { + DismStateNotPresent = 0, + DismStateUninstallPending, + DismStateStaged, + DismStateResolved, // For internal use only + DismStateRemoved = DismStateResolved, + DismStateInstalled, + DismStateInstallPending, + DismStateSuperseded, + DismStatePartiallyInstalled + } DismPackageFeatureState; + + typedef enum _DismRestartType + { + DismRestartNo = 0, + DismRestartPossible, + DismRestartRequired + } DismRestartType; + +#pragma pack(push, 1) + + typedef struct _DismCustomProperty + { + PCWSTR Name; + PCWSTR Value; + PCWSTR Path; + } DismCustomProperty; + + typedef struct _DismFeature + { + PCWSTR FeatureName; + DismPackageFeatureState State; + } DismFeature; + + typedef struct _DismFeatureInfo + { + PCWSTR FeatureName; + DismPackageFeatureState FeatureState; + PCWSTR DisplayName; + PCWSTR Description; + DismRestartType RestartRequired; + DismCustomProperty* CustomProperty; + UINT CustomPropertyCount; + } DismFeatureInfo; + +#pragma pack(pop) + +#ifdef __cplusplus + } +#endif + +#endif /* WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP | WINAPI_PARTITION_PKG_DISM) */ +#pragma endregion + +#endif // _DISMAPI_H_ + + using DismInitializePtr = HRESULT(WINAPI*)(int, PCWSTR, PCWSTR); + using DismOpenSessionPtr = HRESULT(WINAPI*)(PCWSTR, PCWSTR, PCWSTR, DismSession*); + using DismCloseSessionPtr = HRESULT(WINAPI*)(DismSession); + using DismShutdownPtr = HRESULT(WINAPI*)(); + using DismGetFeatureInfoPtr = HRESULT(WINAPI*)(UINT, PCWSTR, PCWSTR, DismPackageIdentifier, DismFeatureInfo**); + using DismEnableFeaturePtr = HRESULT(WINAPI*)(UINT, PCWSTR, PCWSTR, DismPackageIdentifier, BOOL, PCWSTR*, UINT, BOOL, HANDLE, DISM_PROGRESS_CALLBACK, PVOID); + using DismDisableFeaturePtr = HRESULT(WINAPI*)(UINT, PCWSTR, PCWSTR, BOOL, HANDLE, DISM_PROGRESS_CALLBACK, PVOID); + using DismDeletePtr = HRESULT(WINAPI*)(VOID*); + + // Forward declaration + struct DismHelper; + + /// + /// Struct representation of a single Windows Feature. + /// + struct WindowsFeature + { + friend DismHelper; + + ~WindowsFeature(); + + // TODO: Implement progress via DismProgressFunction + HRESULT Enable(IProgressCallback& progress); + HRESULT Disable(); + bool DoesExist(); + bool IsEnabled(); + Utility::LocIndString GetDisplayName(); + DismRestartType GetRestartRequiredStatus(); + + DismPackageFeatureState GetState() + { + return m_featureInfo->FeatureState; + } + + protected: + WindowsFeature(std::shared_ptr dismHelper, const std::string& name); + + private: + void GetFeatureInfo(); + + std::string m_featureName; + std::shared_ptr m_dismHelper; + DismFeatureInfo* m_featureInfo = nullptr; + }; + + struct DismHelper : public std::enable_shared_from_this + { + DismHelper(); + ~DismHelper(); + + WindowsFeature GetWindowsFeature(const std::string& featureName) + { + return WindowsFeature(std::move(GetPtr()), featureName); + } + + HRESULT EnableFeature( + PCWSTR featureName, + PCWSTR identifier, + DismPackageIdentifier packageIdentifier, + BOOL limitAccess, + PCWSTR* sourcePaths, + UINT sourcePathCount, + BOOL enableAll, + HANDLE cancelEvent, + DISM_PROGRESS_CALLBACK progress, + PVOID userData); + + HRESULT DisableFeature(PCWSTR featureName, PCWSTR packageName, BOOL removePayload, HANDLE cancelEvent, DISM_PROGRESS_CALLBACK progress, PVOID userData); + + HRESULT GetFeatureInfo(PCWSTR featureName, PCWSTR identifier, DismPackageIdentifier packageIdentifier, DismFeatureInfo** featureInfo); + + HRESULT Delete(VOID* dismStructure); + + private: + typedef UINT DismSession; + + wil::unique_hmodule m_module; + DismSession m_dismSession = DISM_SESSION_DEFAULT; + + DismInitializePtr m_dismInitialize = nullptr; + DismOpenSessionPtr m_dismOpenSession = nullptr; + DismGetFeatureInfoPtr m_dismGetFeatureInfo = nullptr; + DismEnableFeaturePtr m_dismEnableFeature = nullptr; + DismDisableFeaturePtr m_dismDisableFeature = nullptr; + DismCloseSessionPtr m_dismCloseSession = nullptr; + DismShutdownPtr m_dismShutdown = nullptr; + DismDeletePtr m_dismDelete = nullptr; + + std::shared_ptr GetPtr() + { + return shared_from_this(); + } + + void Initialize(); + void OpenSession(); + void CloseSession(); + void Shutdown(); + + template + FuncType GetProcAddressHelper(HMODULE module, LPCSTR functionName); + }; +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 77b119e68c..c02c631667 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -263,6 +263,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFPinning) WINGET_VALIDATE_PASS_THROUGH(EFUninstallPreviousArgument) WINGET_VALIDATE_PASS_THROUGH(EFConfiguration) + WINGET_VALIDATE_PASS_THROUGH(EFWindowsFeature) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) WINGET_VALIDATE_PASS_THROUGH(EnableSelfInitiatedMinidump) diff --git a/src/AppInstallerCommonCore/WindowsFeature.cpp b/src/AppInstallerCommonCore/WindowsFeature.cpp new file mode 100644 index 0000000000..d21875c60f --- /dev/null +++ b/src/AppInstallerCommonCore/WindowsFeature.cpp @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/WindowsFeature.h" +#include "Public/AppInstallerLogging.h" +#include "Public/AppInstallerStrings.h" + +namespace AppInstaller::WindowsFeature +{ +#ifndef AICLI_DISABLE_TEST_HOOKS + static bool s_MockDismHelper_Override = false; + + void TestHook_MockDismHelper_Override(bool status) + { + s_MockDismHelper_Override = status; + } +#endif + + DismHelper::DismHelper() + { +#ifndef AICLI_DISABLE_TEST_HOOKS + // The entire DismHelper class and its functions needs to be mocked since DismHost.exe inherits log file handles. + // Without this, the unit tests will fail to complete waiting for DismHost.exe to release the log file handles. + if (s_MockDismHelper_Override) + { + return; + } +#endif + + m_module.reset(LoadLibraryEx(L"dismapi.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32)); + + if (!m_module) + { + AICLI_LOG(Core, Error, << "Could not load dismapi.dll"); + THROW_LAST_ERROR(); + } + + m_dismInitialize = GetProcAddressHelper(m_module.get(), "DismInitialize"); + m_dismOpenSession = GetProcAddressHelper(m_module.get(), "DismOpenSession"); + m_dismGetFeatureInfo = GetProcAddressHelper(m_module.get(), "DismGetFeatureInfo"); + m_dismEnableFeature = GetProcAddressHelper(m_module.get(), "DismEnableFeature"); + m_dismDisableFeature = GetProcAddressHelper(m_module.get(), "DismDisableFeature"); + m_dismDelete = GetProcAddressHelper(m_module.get(), "DismDelete"); + m_dismCloseSession = GetProcAddressHelper(m_module.get(), "DismCloseSession"); + m_dismShutdown = GetProcAddressHelper(m_module.get(), "DismShutdown"); + + Initialize(); + OpenSession(); + } + + DismHelper::~DismHelper() + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_MockDismHelper_Override) + { + return; + } +#endif + CloseSession(); + Shutdown(); + }; + + template + FuncType DismHelper::GetProcAddressHelper(HMODULE module, LPCSTR functionName) + { + FuncType result = reinterpret_cast(GetProcAddress(module, functionName)); + if (!result) + { + AICLI_LOG(Core, Error, << "Could not get proc address of " << functionName); + THROW_LAST_ERROR(); + } + return result; + } + + void DismHelper::Initialize() + { + LOG_IF_FAILED(m_dismInitialize(DismLogErrorsWarningsInfo, NULL, NULL)); + } + + void DismHelper::OpenSession() + { + LOG_IF_FAILED(m_dismOpenSession(DISM_ONLINE_IMAGE, NULL, NULL, &m_dismSession)); + } + + void DismHelper::CloseSession() + { + LOG_IF_FAILED(m_dismCloseSession(m_dismSession)); + } + + HRESULT DismHelper::EnableFeature( + PCWSTR featureName, + PCWSTR identifier, + DismPackageIdentifier packageIdentifier, + BOOL limitAccess, + PCWSTR* sourcePaths, + UINT sourcePathCount, + BOOL enableAll, + HANDLE cancelEvent, + DISM_PROGRESS_CALLBACK progress, + PVOID userData) + { + return m_dismEnableFeature(m_dismSession, featureName, identifier, packageIdentifier, limitAccess, sourcePaths, sourcePathCount, enableAll, cancelEvent, progress, userData); + } + + HRESULT DismHelper::DisableFeature(PCWSTR featureName, PCWSTR packageName, BOOL removePayload, HANDLE cancelEvent, DISM_PROGRESS_CALLBACK progress, PVOID userData) + { + return m_dismDisableFeature(m_dismSession, featureName, packageName, removePayload, cancelEvent, progress, userData); + } + + HRESULT DismHelper::GetFeatureInfo(PCWSTR featureName, PCWSTR identifier, DismPackageIdentifier packageIdentifier, DismFeatureInfo** featureInfo) + { + return m_dismGetFeatureInfo(m_dismSession, featureName, identifier, packageIdentifier, featureInfo); + } + + HRESULT DismHelper::Delete(VOID* dismStructure) + { + return m_dismDelete(dismStructure); + } + + void DismHelper::Shutdown() + { + LOG_IF_FAILED(m_dismShutdown()); + } + + WindowsFeature::WindowsFeature(std::shared_ptr dismHelper, const std::string& name) + : m_dismHelper(dismHelper), m_featureName(name) + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_MockDismHelper_Override) + { + return; + } +#endif + GetFeatureInfo(); + } + + WindowsFeature::~WindowsFeature() + { + if (m_featureInfo) + { + LOG_IF_FAILED(m_dismHelper->Delete(m_featureInfo)); + } + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + static HRESULT* s_EnableWindowsFeatureResult_TestHook_Override = nullptr; + + void TestHook_SetEnableWindowsFeatureResult_Override(HRESULT* result) + { + s_EnableWindowsFeatureResult_TestHook_Override = result; + } + + static bool* s_DoesWindowsFeatureExistResult_TestHook_Override = nullptr; + + void TestHook_SetDoesWindowsFeatureExistResult_Override(bool* result) + { + s_DoesWindowsFeatureExistResult_TestHook_Override = result; + } + + static bool* s_IsWindowsFeatureEnabledResult_TestHook_Override = nullptr; + + void TestHook_SetIsWindowsFeatureEnabledResult_Override(bool* status) + { + s_IsWindowsFeatureEnabledResult_TestHook_Override = status; + } + + static Utility::LocIndString* s_WindowsFeatureGetDisplayNameResult_TestHook_Override = nullptr; + + void TestHook_SetWindowsFeatureGetDisplayNameResult_Override(Utility::LocIndString* displayName) + { + s_WindowsFeatureGetDisplayNameResult_TestHook_Override = displayName; + } + + static DismRestartType* s_WindowsFeatureGetRestartRequiredStatusResult_TestHook_Override = nullptr; + + void TestHook_SetWindowsFeatureGetRestartStatusResult_Override(DismRestartType* restartType) + { + s_WindowsFeatureGetRestartRequiredStatusResult_TestHook_Override = restartType; + } +#endif + + HRESULT WindowsFeature::Enable(AppInstaller::IProgressCallback& progress) + { + UNREFERENCED_PARAMETER(progress); + +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_EnableWindowsFeatureResult_TestHook_Override) + { + return *s_EnableWindowsFeatureResult_TestHook_Override; + } +#endif + HRESULT hr = m_dismHelper->EnableFeature(Utility::ConvertToUTF16(m_featureName).c_str(), NULL, DismPackageNone, FALSE, NULL, NULL, FALSE, NULL, NULL, NULL); + LOG_IF_FAILED(hr); + return hr; + } + + HRESULT WindowsFeature::Disable() + { + HRESULT hr = m_dismHelper->DisableFeature(Utility::ConvertToUTF16(m_featureName).c_str(), NULL, FALSE, NULL, NULL, NULL); + LOG_IF_FAILED(hr); + return hr; + } + + bool WindowsFeature::DoesExist() + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_DoesWindowsFeatureExistResult_TestHook_Override) + { + return *s_DoesWindowsFeatureExistResult_TestHook_Override; + } +#endif + return m_featureInfo; + } + + bool WindowsFeature::IsEnabled() + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_IsWindowsFeatureEnabledResult_TestHook_Override) + { + return *s_IsWindowsFeatureEnabledResult_TestHook_Override; + } +#endif + // Refresh feature info state prior to retrieving state info. + GetFeatureInfo(); + DismPackageFeatureState featureState = GetState(); + AICLI_LOG(Core, Info, << "Feature state of " << m_featureName << " is " << featureState); + return featureState == DismStateInstalled; + } + + Utility::LocIndString WindowsFeature::GetDisplayName() + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_WindowsFeatureGetDisplayNameResult_TestHook_Override) + { + return *s_WindowsFeatureGetDisplayNameResult_TestHook_Override; + } +#endif + return Utility::LocIndString{ Utility::ConvertToUTF8(std::wstring{ m_featureInfo->DisplayName }) }; + } + + DismRestartType WindowsFeature::GetRestartRequiredStatus() + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_WindowsFeatureGetRestartRequiredStatusResult_TestHook_Override) + { + return *s_WindowsFeatureGetRestartRequiredStatusResult_TestHook_Override; + } +#endif + return m_featureInfo->RestartRequired; + } + + void WindowsFeature::GetFeatureInfo() + { + LOG_IF_FAILED(m_dismHelper->GetFeatureInfo(Utility::ConvertToUTF16(m_featureName).c_str(), NULL, DismPackageNone, &m_featureInfo)); + } +} \ No newline at end of file