Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental feature support for enabling Windows Feature dependencies #3005

Merged
merged 31 commits into from
Mar 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -230,6 +233,7 @@ mysilentwithprogress
nameof
nativehandle
NESTEDINSTALLER
netfx
netlify
Newtonsoft
NOEXPAND
Expand All @@ -247,6 +251,7 @@ nuffing
objbase
objidl
ofile
osfhandle
Outptr
packageinuse
packageinusebyapplication
Expand Down Expand Up @@ -397,7 +402,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
ryfu-msft marked this conversation as resolved.
Show resolved Hide resolved
},
```
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 @@ -491,6 +497,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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this already handled by HasAnyOf ? If not, should it be?

{
return;
}

if (rootDependencies.HasAnyOf(DependencyType::WindowsFeature))
{
context << Workflow::EnsureRunningAsAdmin;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Installer dependencies are known before this workflow is ever executed; Specific dependency types are also checked for in the ReportDependencies workflow.

I'm wondering, now that multiple dependency types will be supported, if it makes more sense to set an execution flag for "Contains Package Dependencies" or "Contains Windows Features Dependencies" in the ReportDependencies flow.

With the flags, then would be possible to have a separate workflow around CheckSupportForWindowsFeatures that checks the flag ensures the experimental feature is enabled and user is running as admin, to avoid creating copies of the dependencies data in memory unnecessarily

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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a missing check for a pending reboot if the feature is already enabled?

Thinking of the case where a user attempts an install and gets a failure that a reboot is required. If they attempt to run the install command again without rebooting, the Windows Feature will be shown as enabled(?) and therefore won't trigger the rebootRequired, effectively negating the need to use --force by running the command twice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the condition so that a reboot required is no longer considered as enabled. That means if they run the install command again, the feature would show up as needing a reboot and wouldn't show up as being enabled and a reboot required message would be the shown requiring --force to proceed.

{
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;

This comment was marked as resolved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to report each feature that failed to enable

}
else
{
context.Reporter.Error() << Resource::String::FailedToEnableWindowsFeatureOverrideRequired << std::endl;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@denelon - I may be asking the wrong (scope creep) questions here, so forgive me, but would we want any sort of telemetry around these?

  1. Which features are requested to be enabled
  2. The success / fail rates of enabling features

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be showing each feature as its own item and not aggregating them as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed so that each windows feature is represented as its own item.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good; I'm presuming any telemetry needs can be added once support is no longer experimental

AICLI_TERMINATE_CONTEXT(hr);
}
}
else if (rebootRequired)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this neglects the case where one feature fails to enable, and another has a reboot required. I don't know if we care about that case or not, or if that's even a possibility with the way hr gets handled.

If --force isn't present, I don't think it matters, since both the reboot and the fail would cause the context to terminate, and the termination reason doesn't make much of a difference. If --force is present, the user would only be informed they are bypassing the failed install, and they wouldn't be informed they are also bypassing the reboot requirement.

{
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);
ryfu-msft marked this conversation as resolved.
Show resolved Hide resolved
}
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 <<
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole thing doesn't seem like the right way to handle dependencies. At least, "download" is not consistent with "and also, handle getting all dependencies squared away". So at least maybe change the name of this task?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this out of "download" to the step right before InstallPackageInstaller.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In reality, this is a larger issue than your feature work, and I think it really needs to be addressed before we can enable dependencies on the whole. Neither of us considered that in retrospect.

You should move the workflow task back here (for now); and probably put it before package dependencies. I say that because one of the packages may actually be dependent on the Windows feature (although it really should have declared it). But maybe it is an optional dependency that affects the install time behavior. It is not likely the case that the Windows feature is dependent on any package though.

The issue is that multiple "downloads" are currently allowed via COM, but if dependencies are actually installed as part of "download", then multiple installs are allowed concurrently. So either we need to change the download/install phase split to download everything, then install everything, or we need to support going back and forth between download and install phases as we handle each dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation, I didn't know that COM allowed for multiple downloads. I moved the task back to the suggested location and added a TODO comment explaining the issue that needs to be addressed.

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 @@ -1782,4 +1782,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>
JohnMcPMS marked this conversation as resolved.
Show resolved Hide resolved
<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