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

Invoke ShellExecute on dism.exe for enabling Windows Features #3659

Merged
merged 7 commits into from Oct 11, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/spelling/allow.txt
Expand Up @@ -129,6 +129,7 @@ deserializing
dest
devblogs
differentpath
DISMAPI
DIRECTONLY
distro
dll
Expand Down
3 changes: 2 additions & 1 deletion src/AppInstallerCLICore/Resources.h
Expand Up @@ -157,6 +157,7 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(DownloadDirectoryArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(Downloading);
WINGET_DEFINE_RESOURCE_STRINGID(EnableAdminSettingFailed);
WINGET_DEFINE_RESOURCE_STRINGID(EnableWindowsFeaturesSuccess);
WINGET_DEFINE_RESOURCE_STRINGID(EnablingWindowsFeature);
WINGET_DEFINE_RESOURCE_STRINGID(ErrorCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(ErrorCommandShortDescription);
Expand Down Expand Up @@ -226,7 +227,7 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(IncludeUnknownArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(IncludeUnknownInListArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(IncompatibleArgumentsProvided);
WINGET_DEFINE_RESOURCE_STRINGID(InstallationAbandoned);
WINGET_DEFINE_RESOURCE_STRINGID(InstallAbandoned);
WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer1);
WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer2);
WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimerMSStore);
Expand Down
132 changes: 72 additions & 60 deletions src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp
Expand Up @@ -6,12 +6,11 @@
#include "ManifestComparator.h"
#include "InstallFlow.h"
#include "winget\DependenciesGraph.h"
#include "winget\WindowsFeature.h"
#include "DependencyNodeProcessor.h"
#include "ShellExecuteInstallerHandler.h"

using namespace AppInstaller::Repository;
using namespace AppInstaller::Manifest;
using namespace AppInstaller::WindowsFeature;

namespace AppInstaller::CLI::Workflow
{
Expand Down Expand Up @@ -136,82 +135,91 @@ namespace AppInstaller::CLI::Workflow

const auto& rootDependencies = context.Get<Execution::Data::Installer>()->Dependencies;

if (rootDependencies.Empty())
if (rootDependencies.Empty() || !rootDependencies.HasAnyOf(DependencyType::WindowsFeature))
{
return;
}

if (rootDependencies.HasAnyOf(DependencyType::WindowsFeature))
context << Workflow::EnsureRunningAsAdmin;

if (context.IsTerminated())
{
context << Workflow::EnsureRunningAsAdmin;
if (context.IsTerminated())
return;
}

bool isCancelled = false;
bool enableFeaturesFailed = false;
bool rebootRequired = false;
bool force = context.Args.Contains(Execution::Args::Type::Force);

rootDependencies.ApplyToType(DependencyType::WindowsFeature, [&context, &isCancelled, &enableFeaturesFailed, &force, &rebootRequired](Dependency dependency)
{
return;
}
if (enableFeaturesFailed && !force || isCancelled)
{
return;
}

HRESULT hr = S_OK;
std::shared_ptr<DismHelper> dismHelper = std::make_shared<DismHelper>();
auto featureName = dependency.Id();

bool force = context.Args.Contains(Execution::Args::Type::Force);
bool rebootRequired = false;
auto featureContextPtr = context.CreateSubContext();
Execution::Context& featureContext = *featureContextPtr;
auto previousThreadGlobals = featureContext.SetForCurrentThread();

rootDependencies.ApplyToType(DependencyType::WindowsFeature, [&context, &hr, &dismHelper, &force, &rebootRequired](Dependency dependency)
featureContext << Workflow::ShellExecuteEnableWindowsFeature(featureName);

if (featureContext.IsTerminated())
{
if (SUCCEEDED(hr) || force)
{
auto featureName = dependency.Id();
AICLI_LOG(Core, Verbose, << "Processing Windows Feature dependency [" << featureName << "]");
WindowsFeature::WindowsFeature windowsFeature = dismHelper->GetWindowsFeature(featureName);
isCancelled = true;
return;
}

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;
}
}
});
Utility::LocIndView locIndFeatureName{ featureName };
DWORD result = featureContext.Get<Execution::Data::OperationReturnCode>();

if (FAILED(hr))
{
if (force)
if (result == ERROR_SUCCESS)
{
AICLI_LOG(Core, Info, << "Successfully enabled [" << featureName << "]");
}
else if (result == 0x800f080c) // DISMAPI_E_UNKNOWN_FEATURE
{
AICLI_LOG(Core, Warning, << "Windows Feature [" << featureName << "] does not exist");
enableFeaturesFailed = true;
featureContext.Reporter.Warn() << Resource::String::WindowsFeatureNotFound(locIndFeatureName) << std::endl;
}
else if (result == ERROR_SUCCESS_REBOOT_REQUIRED)
{
context.Reporter.Warn() << Resource::String::FailedToEnableWindowsFeatureOverridden << std::endl;
AICLI_LOG(Core, Info, << "Reboot required for [" << featureName << "]");
rebootRequired = true;
}
else
{
context.Reporter.Error() << Resource::String::FailedToEnableWindowsFeatureOverrideRequired << std::endl;
AICLI_TERMINATE_CONTEXT(hr);
AICLI_LOG(Core, Error, << "Failed to enable Windows Feature [" << featureName << "] with exit code: " << result);
enableFeaturesFailed = true;
featureContext.Reporter.Warn() << Resource::String::FailedToEnableWindowsFeature(locIndFeatureName, result) << std::endl;
}
});

if (isCancelled)
{
context.Reporter.Warn() << Resource::String::InstallAbandoned << std::endl;
AICLI_TERMINATE_CONTEXT(E_ABORT);
}

if (enableFeaturesFailed)
{
if (force)
{
context.Reporter.Warn() << Resource::String::FailedToEnableWindowsFeatureOverridden << std::endl;
}
else if (rebootRequired)
else
{
context.Reporter.Error() << Resource::String::FailedToEnableWindowsFeatureOverrideRequired << std::endl;
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES);
}
}
else
{
if (rebootRequired)
{
if (force)
{
Expand All @@ -223,6 +231,10 @@ namespace AppInstaller::CLI::Workflow
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_TO_INSTALL);
}
}
else
{
context.Reporter.Info() << Resource::String::EnableWindowsFeaturesSuccess << std::endl;
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLICore/Workflows/MsiInstallFlow.cpp
Expand Up @@ -52,7 +52,7 @@ namespace AppInstaller::CLI::Workflow

if (!installResult)
{
context.Reporter.Warn() << Resource::String::InstallationAbandoned << std::endl;
context.Reporter.Warn() << Resource::String::InstallAbandoned << std::endl;
AICLI_TERMINATE_CONTEXT(E_ABORT);
}
else
Expand Down
114 changes: 107 additions & 7 deletions src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp
Expand Up @@ -4,6 +4,7 @@
#include "ShellExecuteInstallerHandler.h"
#include <AppInstallerFileLogger.h>
#include <AppInstallerRuntime.h>
#include <winget/Filesystem.h>

using namespace AppInstaller::CLI;
using namespace AppInstaller::Utility;
Expand All @@ -15,7 +16,7 @@ namespace AppInstaller::CLI::Workflow
namespace
{
// ShellExecutes the given path.
std::optional<DWORD> InvokeShellExecuteEx(const std::filesystem::path& filePath, const std::string& args, bool useRunAs, IProgressCallback& progress)
std::optional<DWORD> InvokeShellExecuteEx(const std::filesystem::path& filePath, const std::string& args, bool useRunAs, int show, IProgressCallback& progress)
{
AICLI_LOG(CLI, Info, << "Starting: '" << filePath.u8string() << "' with arguments '" << args << '\'');

Expand All @@ -25,9 +26,7 @@ namespace AppInstaller::CLI::Workflow
execInfo.lpFile = filePath.c_str();
std::wstring argsUtf16 = Utility::ConvertToUTF16(args);
execInfo.lpParameters = argsUtf16.c_str();
// Some installers force UI. Setting to SW_HIDE will hide installer UI and installation will never complete.
// Verified setting to SW_SHOW does not hurt silent mode since no UI will be shown.
execInfo.nShow = SW_SHOW;
execInfo.nShow = show;

// This installer must be run elevated, but we are not currently.
// Have ShellExecute elevate the installer since it won't do so itself.
Expand Down Expand Up @@ -68,7 +67,9 @@ namespace AppInstaller::CLI::Workflow

std::optional<DWORD> InvokeShellExecute(const std::filesystem::path& filePath, const std::string& args, IProgressCallback& progress)
{
return InvokeShellExecuteEx(filePath, args, false, progress);
// Some installers force UI. Setting to SW_HIDE will hide installer UI and installation will never complete.
// Verified setting to SW_SHOW does not hurt silent mode since no UI will be shown.
return InvokeShellExecuteEx(filePath, args, false, SW_SHOW, progress);
}

// Gets the escaped installer args.
Expand Down Expand Up @@ -201,6 +202,45 @@ namespace AppInstaller::CLI::Workflow

return args;
}

std::filesystem::path GetDismExecutablePath()
{
return AppInstaller::Filesystem::GetExpandedPath("%windir%\\system32\\dism.exe");
}

std::optional<DWORD> DoesWindowsFeatureExist(Execution::Context& context, std::string_view featureName)
{
std::string args = "/Online /Get-FeatureInfo /FeatureName:" + std::string{ featureName };
auto dismExecPath = GetDismExecutablePath();

auto getFeatureInfoResult = context.Reporter.ExecuteWithProgress(
std::bind(InvokeShellExecuteEx,
dismExecPath,
args,
false,
SW_HIDE,
std::placeholders::_1));

return getFeatureInfoResult;
}

std::optional<DWORD> EnableWindowsFeature(Execution::Context& context, std::string_view featureName)
{
std::string args = "/Online /Enable-Feature /NoRestart /FeatureName:" + std::string{ featureName };
auto dismExecPath = GetDismExecutablePath();

AICLI_LOG(Core, Info, << "Enabling Windows Feature [" << featureName << "]");

auto enableFeatureResult = context.Reporter.ExecuteWithProgress(
std::bind(InvokeShellExecuteEx,
dismExecPath,
args,
false,
SW_HIDE,
std::placeholders::_1));

return enableFeatureResult;
}
}

void ShellExecuteInstallImpl(Execution::Context& context)
Expand All @@ -221,16 +261,19 @@ namespace AppInstaller::CLI::Workflow
context.Reporter.Warn() << Resource::String::InstallerElevationExpected << std::endl;
}

// Some installers force UI. Setting to SW_HIDE will hide installer UI and installation will never complete.
// Verified setting to SW_SHOW does not hurt silent mode since no UI will be shown.
auto installResult = context.Reporter.ExecuteWithProgress(
std::bind(InvokeShellExecuteEx,
context.Get<Execution::Data::InstallerPath>(),
installerArgs,
installer->ElevationRequirement == ElevationRequirementEnum::ElevationRequired && !isElevated,
SW_SHOW,
std::placeholders::_1));

if (!installResult)
{
context.Reporter.Warn() << Resource::String::InstallationAbandoned << std::endl;
context.Reporter.Warn() << Resource::String::InstallAbandoned << std::endl;
AICLI_TERMINATE_CONTEXT(E_ABORT);
}
else
Expand Down Expand Up @@ -307,8 +350,65 @@ namespace AppInstaller::CLI::Workflow
else
{
context.Add<Execution::Data::OperationReturnCode>(uninstallResult.value());

}
}
}

#ifndef AICLI_DISABLE_TEST_HOOKS
std::optional<DWORD> s_EnableWindowsFeatureResult_Override{};
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: should we use the pointer pattern for this one and below like other test hooks so the codes have consistency?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is another test hook that also passes an optional value like so:
void TestHook_SetPinningIndex_Override(std::optional<std::filesystem::path>&& indexPath);

Since I am also passing an optional DWORD value, I just did something similar to that.


void TestHook_SetEnableWindowsFeatureResult_Override(std::optional<DWORD>&& result)
{
s_EnableWindowsFeatureResult_Override = std::move(result);
}

std::optional<DWORD> s_DoesWindowsFeatureExistResult_Override{};

void TestHook_SetDoesWindowsFeatureExistResult_Override(std::optional<DWORD>&& result)
{
s_DoesWindowsFeatureExistResult_Override = std::move(result);
}
#endif

void ShellExecuteEnableWindowsFeature::operator()(Execution::Context& context) const
{
Utility::LocIndView locIndFeatureName{ m_featureName };

#ifndef AICLI_DISABLE_TEST_HOOKS
auto doesFeatureExistResult = s_DoesWindowsFeatureExistResult_Override.has_value() ?
s_DoesWindowsFeatureExistResult_Override.value() :
DoesWindowsFeatureExist(context, m_featureName);
#else
auto doesFeatureExistResult = DoesWindowsFeatureExist(context, featureName);
#endif

if (!doesFeatureExistResult)
{
AICLI_TERMINATE_CONTEXT(E_ABORT);
}
else if (doesFeatureExistResult.value() != ERROR_SUCCESS)
{
context.Add<Execution::Data::OperationReturnCode>(doesFeatureExistResult.value());
return;
}

context.Reporter.Info() << Resource::String::EnablingWindowsFeature(locIndFeatureName) << std::endl;

#ifndef AICLI_DISABLE_TEST_HOOKS
auto enableFeatureResult = s_EnableWindowsFeatureResult_Override.has_value() ?
s_EnableWindowsFeatureResult_Override.value() :
EnableWindowsFeature(context, m_featureName);
#else
auto enableFeatureResult = EnableWindowsFeature(context, featureName);
#endif

if (!enableFeatureResult)
{
AICLI_TERMINATE_CONTEXT(E_ABORT);
}
else
{
context.Add<Execution::Data::OperationReturnCode>(enableFeatureResult.value());
}
}
}