Skip to content

Commit

Permalink
Add experimental feature support for enabling Windows Feature depende…
Browse files Browse the repository at this point in the history
…ncies (#3005)
  • Loading branch information
ryfu-msft authored Mar 24, 2023
1 parent c5f7df4 commit b95f9ca
Show file tree
Hide file tree
Showing 26 changed files with 1,078 additions and 5 deletions.
7 changes: 7 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@ contactsupport
contentfiles
contoso
contractversion
corecrt
count'th
countof
countryregion
cplusplus
createmanifestmetadata
cswinrt
ctc
Expand All @@ -84,6 +86,7 @@ deigh
deleteifnotneeded
desktopappinstaller
diskfull
dismapi
dllimport
DMPAs
dnld
Expand Down Expand Up @@ -231,6 +234,7 @@ mysilentwithprogress
nameof
nativehandle
NESTEDINSTALLER
netfx
netlify
Newtonsoft
NOEXPAND
Expand All @@ -252,6 +256,7 @@ nuffing
objbase
objidl
ofile
osfhandle
Outptr
packageinuse
packageinusebyapplication
Expand Down Expand Up @@ -408,7 +413,9 @@ Webserver
websites
WERSJA
wesome
wfopen
Whatif
winapifamily
windir
windowsdeveloper
winerror
Expand Down
11 changes: 11 additions & 0 deletions doc/Settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
```
7 changes: 7 additions & 0 deletions src/AppInstallerCLICore/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
100 changes: 100 additions & 0 deletions src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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))
Expand Down
6 changes: 6 additions & 0 deletions src/AppInstallerCLICore/Workflows/DependenciesFlow.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 2 additions & 0 deletions src/AppInstallerCLICore/Workflows/InstallFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLIE2ETests/FeaturesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
Expand Down
34 changes: 34 additions & 0 deletions src/AppInstallerCLIE2ETests/InstallCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ namespace AppInstallerCLIE2ETests
/// </summary>
public class InstallCommand : BaseCommand
{
/// <summary>
/// One time setup.
/// </summary>
[OneTimeSetUp]
public void OneTimeSetup()
{
WinGetSettingsHelper.ConfigureFeature("windowsFeature", true);
}

/// <summary>
/// Set up.
/// </summary>
Expand Down Expand Up @@ -613,5 +622,30 @@ public void InstallExeWithLatestInstalledWithForce()
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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ public static void InitializeAllFeatures(bool status)
ConfigureFeature("directMSI", status);
ConfigureFeature("pinning", status);
ConfigureFeature("configuration", status);
ConfigureFeature("windowsFeature", status);
}
}
}
30 changes: 30 additions & 0 deletions src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
Original file line number Diff line number Diff line change
Expand Up @@ -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>
4 changes: 4 additions & 0 deletions src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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>
Expand Down
Loading

0 comments on commit b95f9ca

Please sign in to comment.