-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Conversation
|
||
const auto& rootDependencies = context.Get<Execution::Data::Installer>()->Dependencies; | ||
|
||
if (rootDependencies.Empty()) |
There was a problem hiding this comment.
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?
<comment>"Windows Feature" is the name of the options Windows features setting.</comment> | ||
</data> | ||
<data name="EnablingWindowsFeatures" xml:space="preserve"> | ||
<value>Enabling Windows Feature dependencies...</value> |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a symbolic name and a friendly (localized) name. I don't know how easy it is to get the friendly name from the symbolic name. Which do you expect to see (or both)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would think either would work; If it's easy to get the friendly name, that would be great, but I would expect most users could google the symbolic name to find the friendly name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to showing friendly name as well as the symbolic name such as [netfx3]
|
||
if (rootDependencies.HasAnyOf(DependencyType::WindowsFeature)) | ||
{ | ||
context << Workflow::EnsureRunningAsAdmin; |
There was a problem hiding this comment.
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
bool continueOnFailure = context.Args.Contains(Execution::Args::Type::Force); | ||
bool rebootRequired = false; | ||
|
||
context.Reporter.Info() << Resource::String::EnablingWindowsFeatures << std::endl; |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
WindowsFeature::WindowsFeature windowsFeature{ name }; | ||
if (!windowsFeature.DoesExist()) | ||
{ | ||
invalidFeatures.emplace_back(name); |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no longer create vector to aggregate missing dependencies, they will be reported at the end after each windows feature is processed
{ | ||
if (continueOnFailure) | ||
{ | ||
context.Reporter.Warn() << Resource::String::FailedToEnableWindowsFeatureOverridden << std::endl; |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
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
src/AppInstallerCLITests/TestData/InstallFlowTest_WindowsFeatures.yaml
Outdated
Show resolved
Hide resolved
} | ||
else | ||
{ | ||
context.Reporter.Error() << Resource::String::FailedToEnableWindowsFeatureOverrideRequired << std::endl; |
There was a problem hiding this comment.
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?
- Which features are requested to be enabled
- The success / fail rates of enabling features
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
@@ -495,6 +495,7 @@ namespace AppInstaller::CLI::Workflow | |||
Workflow::GetDependenciesFromInstaller << | |||
Workflow::ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << | |||
Workflow::ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << | |||
Workflow::EnableWindowsFeaturesDependencies << |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
HRESULT DisableFeature(); | ||
|
||
private: | ||
DismApiHelper m_dismApiHelper; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a lot of repeated loading and probing for the functions using this pattern. It would be much better to just have this created one time.
If you want dism to be able to unload (which seems reasonable), then I would design this differently as well. I would then create a single DismHelper
object in the workflow and have it create WindowsFeature
objects for you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redesigned the implementation so that a single DismHelper object is initialized and it creates a WindowsFeature
object for each feature dependency.
|
||
wil::unique_hmodule m_module; | ||
DismSession m_session = DISM_SESSION_DEFAULT; | ||
DismFeatureInfo* m_featureInfo = nullptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't be holding state that is feature specific.
You shouldn't be using a raw pointer if you can help it, use a smart pointer type instead. Both std::unique_ptr
and wil::unique_any
can handle this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried using a unique_ptr but I don't think it's possible to access the memory address of a unique_ptr which is what I need for the **FeatureInfo parameter in DismFeatureInfo. I believe this is okay though because DismDelete should handle the cleanup of this pointer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could do this with wil::unique_any
, but I'm not sure that it supports a dynamic delete pointer. But we can live without it.
DismFeatureInfo* featureInfo = m_dismApiHelper.GetFeatureInfo(m_featureName); | ||
DismPackageFeatureState featureState = featureInfo->FeatureState; | ||
AICLI_LOG(Core, Verbose, << "Feature state of " << m_featureName << " is " << featureState); | ||
return (featureState == DismStateInstalled || featureState == DismStateInstallPending); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have more information on why DismStateInstallPending
should be included but DismStatePartiallyInstalled
is not? My gut would be that only DismStateInstalled
would be installed and everything else should require us to do something.
There was a problem hiding this comment.
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 only DismStateInstalled
is considered as enabled.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
AICLI_TERMINATE_CONTEXT(hr); | ||
} | ||
} | ||
else if (rebootRequired) |
There was a problem hiding this comment.
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 (windowsFeature.DoesExist()) | ||
{ | ||
if (!windowsFeature.IsEnabled()) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
||
info << Resource::String::EnablingWindowsFeature(Utility::LocIndView{ featureDisplayName }, locIndFeatureName) << std::endl; | ||
|
||
hr = windowsFeature.Enable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be using the reporter's "execute with progress" system, even if it doesn't have any actual progress. That way it at least shows the spinner while it is doing a presumably long running task.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed Enable to take in an IProgressCallback parameter and declared it as an unreferenced parameter. Verified spinner works and is shown manually.
@@ -495,6 +495,7 @@ namespace AppInstaller::CLI::Workflow | |||
Workflow::GetDependenciesFromInstaller << | |||
Workflow::ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << | |||
Workflow::ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << | |||
Workflow::EnableWindowsFeaturesDependencies << |
There was a problem hiding this comment.
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.
|
||
std::string m_featureName; | ||
DismFeatureInfo* m_featureInfo = nullptr; | ||
DismGetFeatureInfoPtr m_getFeatureInfoPtr = nullptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The correct way to do this would be using a shared_ptr<DismHelper>
that the features would hold to keep the helper from being destroyed before them. Then the features would call through that to the functions that it holds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved WindowsFeature out of DismHelper and created a shared_ptr<DismHelper>
member. WindowsFeature now calls the functions through the dismhelper shared pointer.
DismHelper::DismHelper() | ||
{ | ||
#ifndef AICLI_DISABLE_TEST_HOOKS | ||
// The entire DismHelper class needs to be mocked since DismHost.exe inherits log file handles. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't need to change, but it would be a lot cleaner:
Given that this is almost exclusively a function pointer holder, I would have used a derived type for the test override and create function that enabled me to inject the mock (otherwise creating the base type). That would have required a lot fewer test hook points.
if (!m_dismInitialize) | ||
{ | ||
AICLI_LOG(Core, Error, << "Could not get proc address of DismInitialize"); | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RE: The other comment:
This should throw, not return an object that can't perform it's duties (unless there is a set of these functions that isn't needed in a specific case).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to using suggested helper function. I chose to use THROW_LAST_ERROR
since that returns the error code returned from GetProcAddress
{ | ||
if (m_dismInitialize) | ||
{ | ||
LOG_IF_FAILED(m_dismInitialize(2, NULL, NULL)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2? Comment or constant that indicates what/why that is being used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to using enum value DismLogErrorsWarningsInfo
@@ -1804,8 +1804,9 @@ Please specify one of them using the --source option to proceed.</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}] feature.</value> | |||
<comment>{Locked="{0}"} Error message displayed when a failure was encountered when enabling a Windows Feature.</comment> | |||
<value>Failed to enable [{0}] feature with exit code: {1}</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<value>Failed to enable [{0}] feature with exit code: {1}</value> | |
<value>Failed to enable {0} [{1}] feature. | |
{2}: {3}</value> |
I was looking for something with more information directly, showing the message as well. Example:
Failed to enable Windows Sandbox [windows-sandbox] feature.
0x80001234: The system error message for the HRESULT.
0 = Display Name
1 = Feature Name
2 = HRESULT
3 = HRESULT description
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to suggested, I used GetPresentableUserMessage to handle {2}: {3}
|
||
private: | ||
friend WindowsFeature; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't look like it needs to be a friend if this object simply exposed public methods for calling into the function pointers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed and exposed public functions
{ | ||
// This default constructor is only used for mocking unit tests. | ||
WindowsFeature() = default; | ||
WindowsFeature(std::shared_ptr<DismHelper> dismHelper, const std::string& name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I find this awkward and would have a helper on DismHelper
(that also requires that DismHelper
derive from std::enable_shared_from_this
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left the constructor as is to be able to get a shared_ptr to DismHelper. I changed DismHelper to derive from enable_shared_from_this
so that we can safely get another shared_ptr as suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The awkward part is the call site; the need to pass the shared_ptr
yourself. (I should have put the comment there I suppose...) I wasn't suggesting that you should take the constructor away (although you could make it protected and friend the other way). I was suggesting that you add a function like:
WindowsFeature DismHelper::GetFeature(std::string featureName);
So that the call site was less awkward, like:
WindowsFeature::WindowsFeature windowsFeature = dismHelper->GetFeature(featureName};
If you did make the constructor protected, you then don't have to worry about the shared_ptr
being null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree the call site is weird passing shared ptrs around. I feel that this is important if dismhelper were to be extended to other scenarios so I changed it based on your suggestions. I created a helper function within dismhelper that creates the windows object for you, calling the constructor with the shared ptr. I made the WindowsFeature constructor protected and made dismHelper a friend so that only the dismhelper can create the WindowsFeature object.
Related to: #556
Description:
This PR adds experimental feature support for enabling Windows Features prior to installation. There are known packages such as Wix toolkit that require certain optional Windows Features to be enabled before installation can run to completion.
Changes:
windowsFeature
experimental feature to the settings schema.Implementation flow:
--ignore-missing-dependencies
flag to bypass this check and continue with the installation.--force
to bypass this failure and continue with installation. Often times, a reboot will be required to fully enable a feature, in this scenario we warn the user but continue with the installation as it is possible that the installation may still succeed.Tests:
netfx3
and disablingnetfx3
and verifying the output is successful.Microsoft Reviewers: Open in CodeFlow