Skip to content

Commit

Permalink
Invoke ShellExecute on dism.exe for enabling Windows Features (#3659)
Browse files Browse the repository at this point in the history
  • Loading branch information
ryfu-msft authored Oct 11, 2023
1 parent 18df795 commit 3d60cec
Show file tree
Hide file tree
Showing 14 changed files with 236 additions and 676 deletions.
1 change: 1 addition & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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{};

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());
}
}
}
Loading

0 comments on commit 3d60cec

Please sign in to comment.