winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit b95f9ca7d2b56dffdc9f4df4a1174daa16581e25
parent c5f7df49fefc52fae9de42381a8559481b790439
Author: Ryan Fu <69221034+ryfu-msft@users.noreply.github.com>
Date:   Fri, 24 Mar 2023 12:55:37 -0700

Add experimental feature support for enabling Windows Feature dependencies (#3005)


Diffstat:
M.github/actions/spelling/expect.txt | 7+++++++
Mdoc/Settings.md | 12++++++++++++
Msrc/AppInstallerCLICore/Resources.h | 7+++++++
Msrc/AppInstallerCLICore/Workflows/DependenciesFlow.cpp | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Workflows/DependenciesFlow.h | 6++++++
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 2++
Msrc/AppInstallerCLIE2ETests/FeaturesCommand.cs | 1+
Msrc/AppInstallerCLIE2ETests/InstallCommand.cs | 34++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.WindowsFeature.yaml | 26++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs | 1+
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 30++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 4++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 6++++++
Asrc/AppInstallerCLITests/TestData/InstallFlowTest_WindowsFeatures.yaml | 24++++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestHooks.h | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLITests/WindowsFeature.cpp | 213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj | 2++
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters | 6++++++
Msrc/AppInstallerCommonCore/ExperimentalFeature.cpp | 4++++
Msrc/AppInstallerCommonCore/FileLogger.cpp | 26+++++++++++++++++++++-----
Msrc/AppInstallerCommonCore/Public/AppInstallerFileLogger.h | 2++
Msrc/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h | 1+
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 2++
Asrc/AppInstallerCommonCore/Public/winget/WindowsFeature.h | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/UserSettings.cpp | 1+
Asrc/AppInstallerCommonCore/WindowsFeature.cpp | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
26 files changed, 1084 insertions(+), 5 deletions(-)

diff --git 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 @@ -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 @@ -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 @@ -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<Execution::Data::Installer>()->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> dismHelper = std::make_shared<DismHelper>(); + + 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 @@ -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 @@ -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 @@ -53,6 +53,7 @@ namespace AppInstallerCLIE2ETests 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 @@ -15,6 +15,15 @@ namespace AppInstallerCLIE2ETests public class InstallCommand : BaseCommand { /// <summary> + /// One time setup. + /// </summary> + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("windowsFeature", true); + } + + /// <summary> /// Set up. /// </summary> [SetUp] @@ -613,5 +622,30 @@ namespace AppInstallerCLIE2ETests Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(baseDir)); Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/execustom")); } + + /// <summary> + /// Test install a package with an invalid Windows Feature dependency. + /// </summary> + [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.")); + } + + /// <summary> + /// Test install a package with a Windows Feature dependency using the force argument. + /// </summary> + [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 @@ -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: <EXEHASH> + InstallerSwitches: + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /LogFile <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> + 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 @@ -191,6 +191,7 @@ namespace AppInstallerCLIE2ETests 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 @@ -1791,4 +1791,34 @@ Please specify one of them using the --source option to proceed.</value> <data name="FileNotFound" xml:space="preserve"> <value>File not found.</value> </data> + <data name="WindowsFeatureNotFound" xml:space="preserve"> + <value>The feature [{0}] was not found.</value> + <comment>{Locked="{0}"} Error message displayed when a Windows feature was not found on the system.</comment> + </data> + <data name="FailedToEnableWindowsFeatureOverrideRequired" xml:space="preserve"> + <value>Failed to enable Windows Feature dependencies. To proceed with installation, use '--force'.</value> + <comment>{Locked="--force"}</comment> + </data> + <data name="RebootRequiredToEnableWindowsFeatureOverrideRequired" xml:space="preserve"> + <value>Reboot required to fully enable the Windows Feature(s); to override this check use '--force'.</value> + <comment>"Windows Feature(s)" is the name of the options Windows features setting.</comment> + </data> + <data name="FailedToEnableWindowsFeatureOverridden" xml:space="preserve"> + <value>Failed to enable Windows Feature dependencies; proceeding due to --force</value> + <comment>"Windows Feature(s)" is the name of the options Windows features setting. +{Locked="--force"}</comment> + </data> + <data name="EnablingWindowsFeature" xml:space="preserve"> + <value>Enabling {0} [{1}]...</value> + <comment>{Locked="{0}","{1}"} Message displayed to the user regarding which Windows Feature is being enabled.</comment> + </data> + <data name="FailedToEnableWindowsFeature" xml:space="preserve"> + <value>Failed to enable {0} [{1}] feature.</value> + <comment>{Locked="{0}"} Windows Feature display name +{Locked="{1}"} Windows Feature name</comment> + </data> + <data name="RebootRequiredToEnableWindowsFeatureOverridden" xml:space="preserve"> + <value>Reboot required to fully enable the Windows Feature(s); proceeding due to --force</value> + <comment>"Windows Feature(s)" is the name of the options Windows features setting.</comment> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -250,6 +250,7 @@ <ClCompile Include="UpdateFlow.cpp" /> <ClCompile Include="UserSettings.cpp" /> <ClCompile Include="Versions.cpp" /> + <ClCompile Include="WindowsFeature.cpp" /> <ClCompile Include="WorkFlow.cpp" /> <ClCompile Include="LanguageUtilities.cpp" /> <ClCompile Include="main.cpp"> @@ -329,6 +330,9 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_UnsupportedArguments.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_WindowsFeatures.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_Zip_Exe.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -290,6 +290,9 @@ <ClCompile Include="TestConfiguration.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="WindowsFeature.cpp"> + <Filter>Source Files\CLI</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> @@ -612,6 +615,9 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_UnsupportedArguments.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_WindowsFeatures.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_Zip_Exe.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_WindowsFeatures.yaml 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 @@ -11,6 +11,7 @@ #include <AppInstallerRuntime.h> #include <winget/UserSettings.h> #include <winget/Filesystem.h> +#include <winget/WindowsFeature.h> #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 @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "WorkflowCommon.h" +#include <AppInstallerRuntime.h> +#include <Commands/InstallCommand.h> +#include <winget/WindowsFeature.h> +#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<Setting::EFWindowsFeature>(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<Setting::EFWindowsFeature>(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<Setting::EFWindowsFeature>(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<Setting::EFWindowsFeature>(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<Setting::EFWindowsFeature>(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 @@ -328,6 +328,7 @@ <ClInclude Include="Public\winget\ThreadGlobals.h" /> <ClInclude Include="Public\winget\TraceLogger.h" /> <ClInclude Include="Public\winget\UserSettings.h" /> + <ClInclude Include="Public\winget\WindowsFeature.h" /> <ClInclude Include="Telemetry\MicrosoftTelemetry.h" /> <ClInclude Include="Telemetry\TraceLogging.h" /> <ClInclude Include="Telemetry\WinEventLogLevels.h" /> @@ -397,6 +398,7 @@ <ClCompile Include="ThreadGlobals.cpp" /> <ClCompile Include="TraceLogger.cpp" /> <ClCompile Include="UserSettings.cpp" /> + <ClCompile Include="WindowsFeature.cpp" /> </ItemGroup> <ItemGroup> <None Include="packages.config" /> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -189,6 +189,9 @@ <ClInclude Include="Public\winget\Pin.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\WindowsFeature.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -335,6 +338,9 @@ <ClCompile Include="Pin.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="WindowsFeature.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -48,6 +48,8 @@ namespace AppInstaller::Settings return userSettings.Get<Setting::EFUninstallPreviousArgument>(); case ExperimentalFeature::Feature::Configuration: return userSettings.Get<Setting::EFConfiguration>(); + case ExperimentalFeature::Feature::WindowsFeature: + return userSettings.Get<Setting::EFWindowsFeature>(); 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 @@ -6,6 +6,7 @@ #include "Public/AppInstallerRuntime.h" #include "Public/AppInstallerStrings.h" #include "Public/AppInstallerDateTime.h" +#include <corecrt_io.h> 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<HANDLE>(_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 @@ -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 @@ -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 @@ -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 @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <AppInstallerProgress.h> +#include <winget/LocIndependent.h> + +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 <winapifamily.h> + +#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; + + /// <summary> + /// Struct representation of a single Windows Feature. + /// </summary> + 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> dismHelper, const std::string& name); + + private: + void GetFeatureInfo(); + + std::string m_featureName; + std::shared_ptr<DismHelper> m_dismHelper; + DismFeatureInfo* m_featureInfo = nullptr; + }; + + struct DismHelper : public std::enable_shared_from_this<DismHelper> + { + 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<DismHelper> GetPtr() + { + return shared_from_this(); + } + + void Initialize(); + void OpenSession(); + void CloseSession(); + void Shutdown(); + + template<typename FuncType> + FuncType GetProcAddressHelper(HMODULE module, LPCSTR functionName); + }; +}+ \ No newline at end of file diff --git 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 @@ -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<DismInitializePtr>(m_module.get(), "DismInitialize"); + m_dismOpenSession = GetProcAddressHelper<DismOpenSessionPtr>(m_module.get(), "DismOpenSession"); + m_dismGetFeatureInfo = GetProcAddressHelper<DismGetFeatureInfoPtr>(m_module.get(), "DismGetFeatureInfo"); + m_dismEnableFeature = GetProcAddressHelper<DismEnableFeaturePtr>(m_module.get(), "DismEnableFeature"); + m_dismDisableFeature = GetProcAddressHelper<DismDisableFeaturePtr>(m_module.get(), "DismDisableFeature"); + m_dismDelete = GetProcAddressHelper<DismDeletePtr>(m_module.get(), "DismDelete"); + m_dismCloseSession = GetProcAddressHelper<DismCloseSessionPtr>(m_module.get(), "DismCloseSession"); + m_dismShutdown = GetProcAddressHelper<DismShutdownPtr>(m_module.get(), "DismShutdown"); + + Initialize(); + OpenSession(); + } + + DismHelper::~DismHelper() + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_MockDismHelper_Override) + { + return; + } +#endif + CloseSession(); + Shutdown(); + }; + + template<typename FuncType> + FuncType DismHelper::GetProcAddressHelper(HMODULE module, LPCSTR functionName) + { + FuncType result = reinterpret_cast<FuncType>(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> 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