commit 827f9ea779b4023635c391cadbd55d9d0d07a047 parent 31a785668e07cf57ecf37e44eb7424b1b415287d Author: Ruben Guerrero <rubengu@microsoft.com> Date: Mon, 28 Aug 2023 12:09:15 -0700 Add ability to specify where to save modules for configuration (#3536) Before this PR, all the modules are installed using Install-Module with its default scope (current user). In the case where other applications (yes, I'm talking about OneDrive) decide to change the user profile while configuration it running, the PowerShell modules placed in MyDocuments\PowerShell\Modules get lost forever (more like moved into the OneDrive location). To mitigate this issue, this PR adds the ability to specify where to install the modules via APIs. Locations CurrentUser: the same location where Install-Module -Scope CurrentUser will install modules. AllUsers: the same location where Install-Module -Scope AllUsers will install modules. WinGetModulePath: the winget specific location %LOCALAPPDATA%\Microsoft\WinGet\Configuration\Modules Custom: Any location the user wants to. Currently, the default location is CurrentUser. Internally, the custom location will only be added to the PSModulePath when specified. The WinGetModulePath is always added to the PSModulePath. When a custom path is provided, it must be an absolute path. Winget flags winget configure file.yml --module-path currentuser winget configure file.yml --module-path allusers winget configure file.yml --module-path default winget configure file.yml --module-path <path> Microsoft.WinGet.Configuration Get-WinGetConfiguration -File file.yml -ModulePath CurrentUser Get-WinGetConfiguration -File file.yml -ModulePath AllUsers Get-WinGetConfiguration -File file.yml -ModulePath Default Get-WinGetConfiguration -File file.yml -ModulePath <path> Diffstat:
45 files changed, 1092 insertions(+), 242 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -8,6 +8,7 @@ AFAIK agg aicli AICLIC +allusers alreadyinstalled AMap amrutha @@ -95,6 +96,7 @@ cplusplus createmanifestmetadata cswinrt ctc +currentuser DACL Dbg debian @@ -116,6 +118,7 @@ dustojnikhummer dvinns ecfr ecfrbrowse +ECustom EFGH EFile endregion diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -377,6 +377,7 @@ <ClInclude Include="Commands\ValidateCommand.h" /> <ClInclude Include="Commands\SettingsCommand.h" /> <ClInclude Include="CompletionData.h" /> + <ClInclude Include="ConfigurationCommon.h" /> <ClInclude Include="ConfigurationContext.h" /> <ClInclude Include="ContextOrchestrator.h" /> <ClInclude Include="COMContext.h" /> @@ -430,6 +431,7 @@ <ClCompile Include="Commands\ImportCommand.cpp" /> <ClCompile Include="Commands\PinCommand.cpp" /> <ClCompile Include="Commands\TestCommand.cpp" /> + <ClCompile Include="ConfigurationCommon.cpp" /> <ClCompile Include="ConfigurationContext.cpp" /> <ClCompile Include="ConfigurationSetProcessorFactoryRemoting.cpp" /> <ClCompile Include="ContextOrchestrator.cpp" /> @@ -503,8 +505,8 @@ <ProjectReference Include="..\YamlCppLib\YamlCppLib.vcxproj"> <Project>{8bb94bb8-374f-4294-bca1-c7811514a6b7}</Project> </ProjectReference> - <ProjectReference Include="..\Microsoft.Management.Configuration.Processor\Microsoft.Management.Configuration.Processor.csproj"> - <ReferenceOutputAssembly>false</ReferenceOutputAssembly> + <ProjectReference Include="..\Microsoft.Management.Configuration.Processor\Microsoft.Management.Configuration.Processor.csproj"> + <ReferenceOutputAssembly>false</ReferenceOutputAssembly> </ProjectReference> </ItemGroup> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> @@ -519,7 +521,6 @@ <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.ImplementationLibrary.1.0.210204.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorTextNuget)', '$(SolutionDir)\packages\Microsoft.Windows.ImplementationLibrary.1.0.210204.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" /> <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.210505.3\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorTextNuget)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.210505.3\build\native\Microsoft.Windows.CppWinRT.props'))" /> <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.210505.3\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorTextNuget)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.210505.3\build\native\Microsoft.Windows.CppWinRT.targets'))" /> - <PropertyGroup> <MicrosoftManagementConfigurationProcessorPath>$(OutputPath)..\Microsoft.Management.Configuration.Processor\Microsoft.Management.Configuration.Processor.winmd</MicrosoftManagementConfigurationProcessorPath> <MicrosoftManagementConfigurationProcessorPath Condition="!Exists('$(MicrosoftManagementConfigurationProcessorPath)')">$(SolutionDir)\AnyCPU\$(Configuration)\Microsoft.Management.Configuration.Processor\net6.0-windows10.0.19041.0\win\Microsoft.Management.Configuration.Processor.winmd</MicrosoftManagementConfigurationProcessorPath> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -227,6 +227,9 @@ <ClInclude Include="Commands\DownloadCommand.h"> <Filter>Commands</Filter> </ClInclude> + <ClInclude Include="ConfigurationCommon.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -418,9 +421,12 @@ <ClCompile Include="Commands\TestCommand.cpp"> <Filter>Commands</Filter> </ClCompile> - <ClInclude Include="Commands\DownloadCommand.cpp"> - <Filter>Commands</Filter> - </ClInclude> + <ClCompile Include="Commands\DownloadCommand.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="ConfigurationCommon.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -183,6 +183,8 @@ namespace AppInstaller::CLI return { type, "enable"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::StubType }; case Execution::Args::Type::ConfigurationDisable: return { type, "disable"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::StubType }; + case Execution::Args::Type::ConfigurationModulePath: + return { type, "module-path"_liv }; // Download command case Execution::Args::Type::DownloadDirectory: diff --git a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp @@ -7,6 +7,7 @@ #include "ConfigureValidateCommand.h" #include "Workflows/ConfigurationFlow.h" #include "Workflows/MSStoreInstallerHandler.h" +#include "ConfigurationCommon.h" using namespace AppInstaller::CLI::Workflow; @@ -31,6 +32,7 @@ namespace AppInstaller::CLI { return { Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional }, + Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath, ArgumentType::Positional }, Argument{ Execution::Args::Type::ConfigurationAcceptWarning, Resource::String::ConfigurationAcceptWarningArgumentDescription, ArgumentType::Flag }, Argument{ Execution::Args::Type::ConfigurationEnable, Resource::String::ConfigurationEnableMessage, ArgumentType::Flag, Argument::Visibility::Help }, Argument{ Execution::Args::Type::ConfigurationDisable, Resource::String::ConfigurationDisableMessage, ArgumentType::Flag, Argument::Visibility::Help }, @@ -89,9 +91,14 @@ namespace AppInstaller::CLI throw CommandException(Resource::String::ConfigurationEnableArgumentError); } } - else if (!execArgs.Contains(Execution::Args::Type::ConfigurationFile)) + else { - throw CommandException(Resource::String::RequiredArgError("file"_liv)); + if (!execArgs.Contains(Execution::Args::Type::ConfigurationFile)) + { + throw CommandException(Resource::String::RequiredArgError("file"_liv)); + } + + Configuration::ValidateCommonArguments(execArgs); } } } diff --git a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "ConfigureShowCommand.h" #include "Workflows/ConfigurationFlow.h" +#include "ConfigurationCommon.h" using namespace AppInstaller::CLI::Workflow; @@ -13,6 +14,7 @@ namespace AppInstaller::CLI return { // Required for now, make exclusive when history implemented Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional, true }, + Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath, ArgumentType::Positional }, }; } @@ -40,4 +42,9 @@ namespace AppInstaller::CLI OpenConfigurationSet << ShowConfigurationSet; } + + void ConfigureShowCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + Configuration::ValidateCommonArguments(execArgs); + } } diff --git a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.h b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.h @@ -18,5 +18,6 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; }; } diff --git a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "ConfigureTestCommand.h" #include "Workflows/ConfigurationFlow.h" +#include "ConfigurationCommon.h" using namespace AppInstaller::CLI::Workflow; @@ -12,6 +13,7 @@ namespace AppInstaller::CLI { return { Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional, true }, + Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath, ArgumentType::Positional }, Argument{ Execution::Args::Type::ConfigurationAcceptWarning, Resource::String::ConfigurationAcceptWarningArgumentDescription, ArgumentType::Flag }, }; } @@ -44,4 +46,9 @@ namespace AppInstaller::CLI ConfirmConfigurationProcessing(false) << TestConfigurationSet; } + + void ConfigureTestCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + Configuration::ValidateCommonArguments(execArgs); + } } diff --git a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.h b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.h @@ -18,5 +18,6 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; }; } diff --git a/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "ConfigureValidateCommand.h" #include "Workflows/ConfigurationFlow.h" +#include "ConfigurationCommon.h" using namespace AppInstaller::CLI::Workflow; @@ -12,6 +13,7 @@ namespace AppInstaller::CLI { return { Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional, true }, + Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath, ArgumentType::Positional }, }; } @@ -42,4 +44,9 @@ namespace AppInstaller::CLI ValidateConfigurationSetUnitProcessors << ValidateAllGoodMessage; } + + void ConfigureValidateCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + Configuration::ValidateCommonArguments(execArgs); + } } diff --git a/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.h b/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.h @@ -18,5 +18,6 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; }; } diff --git a/src/AppInstallerCLICore/ConfigurationCommon.cpp b/src/AppInstallerCLICore/ConfigurationCommon.cpp @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ConfigurationCommon.h" +#include "ExecutionArgs.h" +#include <AppInstallerRuntime.h> +#include "Command.h" +#include <winrt/Microsoft.Management.Configuration.SetProcessorFactory.h> +#include <filesystem> + +namespace AppInstaller::CLI +{ + using namespace winrt::Microsoft::Management::Configuration; + + namespace + { + constexpr std::string_view s_ModulePath_Default = "default"; + constexpr std::string_view s_ModulePath_CurrentUser = "currentuser"; + constexpr std::string_view s_ModulePath_AllUsers = "allusers"; + + struct ModulePathInfo + { + SetProcessorFactory::PwshConfigurationProcessorLocation location; + std::optional<std::string_view> customLocation; + }; + + ModulePathInfo GetModulePathInfo(Execution::Args& execArgs) + { + if (execArgs.Contains(Execution::Args::Type::ConfigurationModulePath)) + { + auto modulePath = execArgs.GetArg(Execution::Args::Type::ConfigurationModulePath); + + if (Utility::CaseInsensitiveEquals(modulePath, s_ModulePath_Default)) + { + return { SetProcessorFactory::PwshConfigurationProcessorLocation::Default, {} }; + } + else if (Utility::CaseInsensitiveEquals(modulePath, s_ModulePath_CurrentUser)) + { + return { SetProcessorFactory::PwshConfigurationProcessorLocation::CurrentUser, {} }; + } + else if (Utility::CaseInsensitiveEquals(modulePath, s_ModulePath_AllUsers)) + { + return { SetProcessorFactory::PwshConfigurationProcessorLocation::AllUsers, {} }; + } + else + { + return { SetProcessorFactory::PwshConfigurationProcessorLocation::Custom, execArgs.GetArg(Execution::Args::Type::ConfigurationModulePath) }; + } + } + + return { SetProcessorFactory::PwshConfigurationProcessorLocation::WinGetModulePath, {} }; + } + } + + namespace Configuration + { + void ValidateCommonArguments(Execution::Args& execArgs) + { + auto modulePath = GetModulePathInfo(execArgs); + + if (modulePath.location == SetProcessorFactory::PwshConfigurationProcessorLocation::AllUsers && !Runtime::IsRunningAsAdmin()) + { + throw CommandException(Resource::String::ConfigurationAllUsersElevated); + } + + if (modulePath.location == SetProcessorFactory::PwshConfigurationProcessorLocation::Custom) + { + auto path = std::filesystem::path{ modulePath.customLocation.value() }; + if (!path.is_absolute()) + { + throw CommandException(Resource::String::ConfigurationModulePathArgError); + } + } + } + + void SetModulePath(Execution::Context& context, IConfigurationSetProcessorFactory const& factory) + { + auto pwshFactory = factory.as<SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties>(); + auto modulePath = GetModulePathInfo(context.Args); + + if (modulePath.location == SetProcessorFactory::PwshConfigurationProcessorLocation::Custom) + { + pwshFactory.CustomLocation(winrt::to_hstring(modulePath.customLocation.value())); + } + + pwshFactory.Location(modulePath.location); + } + } +} diff --git a/src/AppInstallerCLICore/ConfigurationCommon.h b/src/AppInstallerCLICore/ConfigurationCommon.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" +#include <winrt/Microsoft.Management.Configuration.h> + +namespace AppInstaller::CLI +{ + namespace Configuration + { + // Validates common arguments between configuration commands. + void ValidateCommonArguments(Execution::Args& execArgs); + + // Sets the module path to install modules in the set processor. + void SetModulePath(Execution::Context& context, winrt::Microsoft::Management::Configuration::IConfigurationSetProcessorFactory const& factory); + } +} diff --git a/src/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp b/src/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp @@ -223,6 +223,26 @@ namespace AppInstaller::CLI::ConfigurationRemoting m_remoteFactory.as<Processor::IPowerShellConfigurationProcessorFactoryProperties>().Policy(Convert(value)); } + SetProcessorFactory::PwshConfigurationProcessorLocation Location() const + { + return Convert(m_remoteFactory.as<Processor::IPowerShellConfigurationProcessorFactoryProperties>().Location()); + } + + void Location(SetProcessorFactory::PwshConfigurationProcessorLocation value) + { + m_remoteFactory.as<Processor::IPowerShellConfigurationProcessorFactoryProperties>().Location(Convert(value)); + } + + winrt::hstring CustomLocation() const + { + return m_remoteFactory.as<Processor::IPowerShellConfigurationProcessorFactoryProperties>().CustomLocation(); + } + + void CustomLocation(winrt::hstring value) + { + m_remoteFactory.as<Processor::IPowerShellConfigurationProcessorFactoryProperties>().CustomLocation(value); + } + HRESULT STDMETHODCALLTYPE SetLifetimeWatcher(IUnknown* watcher) { return WinRT::LifetimeWatcherBase::SetLifetimeWatcher(watcher); @@ -241,6 +261,18 @@ namespace AppInstaller::CLI::ConfigurationRemoting return ToEnum<Processor::PowerShellConfigurationProcessorPolicy>(ToIntegral(policy)); } + static SetProcessorFactory::PwshConfigurationProcessorLocation Convert(Processor::PowerShellConfigurationProcessorLocation location) + { + // We have used the same values intentionally; if that changes, update this. + return ToEnum<SetProcessorFactory::PwshConfigurationProcessorLocation>(ToIntegral(location)); + } + + static Processor::PowerShellConfigurationProcessorLocation Convert(SetProcessorFactory::PwshConfigurationProcessorLocation location) + { + // We have used the same values intentionally; if that changes, update this. + return ToEnum<Processor::PowerShellConfigurationProcessorLocation>(ToIntegral(location)); + } + IConfigurationSetProcessorFactory m_remoteFactory; wil::unique_event m_completionEvent; Collections::IVector<winrt::hstring> m_additionalModulePaths{ winrt::single_threaded_vector<winrt::hstring>() }; diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -110,6 +110,7 @@ namespace AppInstaller::CLI::Execution ConfigurationAcceptWarning, ConfigurationEnable, ConfigurationDisable, + ConfigurationModulePath, // Common arguments NoVT, // Disable VirtualTerminal outputs diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -50,20 +50,21 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(CompleteCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(CompleteCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationAcceptWarningArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationAllUsersElevated); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationApply); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationAssert); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDependencies); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDescriptionWasTruncated); - WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDisableMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDisabledMessage); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDisableMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationDisablingMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnableArgumentError); - WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnableMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnabledMessage); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnableMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnablingMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToApply); - WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToTest); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToGetDetails); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToTest); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFieldInvalidType); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFieldInvalidValue); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFieldMissing); @@ -78,6 +79,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInitializing); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationLocal); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationModuleNameOnly); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationModulePath); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationModulePathArgError); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationModuleWithDetails); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationNotEnabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationNoTestRun); @@ -130,22 +133,23 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(CountArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(CountOutOfBoundsError); WINGET_DEFINE_RESOURCE_STRINGID(CustomSwitchesArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowContainsLoop); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowDownload); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowInstall); - WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowSourceNotFound); - WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowSourceTooManyMatches); - WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowPackageVersionNotFound); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowNoInstallerFound); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowNoMatches); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowNoMinVersion); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowNoSuitableInstallerFound); - WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowNoMatches); - WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowContainsLoop); - WINGET_DEFINE_RESOURCE_STRINGID(DependenciesSkippedMessage); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowPackageVersionNotFound); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowSourceNotFound); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowSourceTooManyMatches); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesManagementError); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesManagementExitMessage); + WINGET_DEFINE_RESOURCE_STRINGID(DependenciesSkippedMessage); WINGET_DEFINE_RESOURCE_STRINGID(DependencyArgumentMissing); - WINGET_DEFINE_RESOURCE_STRINGID(DisabledByGroupPolicy); + WINGET_DEFINE_RESOURCE_STRINGID(DependencySourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(DisableAdminSettingFailed); + WINGET_DEFINE_RESOURCE_STRINGID(DisabledByGroupPolicy); WINGET_DEFINE_RESOURCE_STRINGID(DisableInteractivityArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Done); WINGET_DEFINE_RESOURCE_STRINGID(DownloadCommandLongDescription); @@ -243,9 +247,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstallerLogAvailable); WINGET_DEFINE_RESOURCE_STRINGID(InstallerProhibitsElevation); WINGET_DEFINE_RESOURCE_STRINGID(InstallerRequiresInstallLocation); - WINGET_DEFINE_RESOURCE_STRINGID(InstallerTypeArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallersAbortTerminal); WINGET_DEFINE_RESOURCE_STRINGID(InstallersRequireInstallLocation); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerTypeArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowInstallSuccess); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowRegistrationDeferred); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowReturnCodeAlreadyInstalled); @@ -371,9 +375,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinType); WINGET_DEFINE_RESOURCE_STRINGID(PinVersion); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesPolicy); + WINGET_DEFINE_RESOURCE_STRINGID(PortableAliasAdded); WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverridden); WINGET_DEFINE_RESOURCE_STRINGID(PortableHashMismatchOverrideRequired); - WINGET_DEFINE_RESOURCE_STRINGID(PortableAliasAdded); WINGET_DEFINE_RESOURCE_STRINGID(PortableInstallFailed); WINGET_DEFINE_RESOURCE_STRINGID(PortableLinksMachine); WINGET_DEFINE_RESOURCE_STRINGID(PortableLinksUser); @@ -478,7 +482,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceAgreementsTitle); WINGET_DEFINE_RESOURCE_STRINGID(SourceArgArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceArgumentDescription); - WINGET_DEFINE_RESOURCE_STRINGID(DependencySourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandLongDescription); @@ -542,10 +545,10 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(UnrecognizedCommand); WINGET_DEFINE_RESOURCE_STRINGID(UnsupportedArgument); WINGET_DEFINE_RESOURCE_STRINGID(UpdateAllArgumentDescription); - WINGET_DEFINE_RESOURCE_STRINGID(UpdateNotApplicable); - WINGET_DEFINE_RESOURCE_STRINGID(UpdateNotApplicableReason); WINGET_DEFINE_RESOURCE_STRINGID(UpdateNoPackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(UpdateNoPackagesFoundReason); + WINGET_DEFINE_RESOURCE_STRINGID(UpdateNotApplicable); + WINGET_DEFINE_RESOURCE_STRINGID(UpdateNotApplicableReason); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeAvailableForPinned); WINGET_DEFINE_RESOURCE_STRINGID(UpgradeBlockedByPinCount); @@ -575,9 +578,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(WindowsFeatureNotFound); WINGET_DEFINE_RESOURCE_STRINGID(WindowsFeaturesDependencies); WINGET_DEFINE_RESOURCE_STRINGID(WindowsLibrariesDependencies); - WINGET_DEFINE_RESOURCE_STRINGID(WindowsStoreTerms); WINGET_DEFINE_RESOURCE_STRINGID(WindowsPackageManager); WINGET_DEFINE_RESOURCE_STRINGID(WindowsPackageManagerPreview); + WINGET_DEFINE_RESOURCE_STRINGID(WindowsStoreTerms); WINGET_DEFINE_RESOURCE_STRINGID(WordArgumentDescription); }; diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -7,6 +7,7 @@ #include <AppInstallerErrors.h> #include <winrt/Microsoft.Management.Configuration.h> #include <winget/SelfManagement.h> +#include "ConfigurationCommon.h" using namespace AppInstaller::CLI::Execution; using namespace winrt::Microsoft::Management::Configuration; @@ -72,7 +73,7 @@ namespace AppInstaller::CLI::Workflow } } - IConfigurationSetProcessorFactory CreateConfigurationSetProcessorFactory() + IConfigurationSetProcessorFactory CreateConfigurationSetProcessorFactory(Execution::Context& context) { #ifndef AICLI_DISABLE_TEST_HOOKS // Test could override the entire workflow task, but that may require keeping more in sync than simply setting the factory. @@ -82,7 +83,9 @@ namespace AppInstaller::CLI::Workflow } #endif - return ConfigurationRemoting::CreateOutOfProcessFactory(); + auto factory = ConfigurationRemoting::CreateOutOfProcessFactory(); + Configuration::SetModulePath(context, factory); + return factory; } std::optional<Utility::LocIndString> GetValueSetString(const ValueSet& valueSet, std::wstring_view value) @@ -779,7 +782,7 @@ namespace AppInstaller::CLI::Workflow auto progressScope = context.Reporter.BeginAsyncProgress(true); progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationInitializing()); - ConfigurationProcessor processor{ CreateConfigurationSetProcessorFactory() }; + ConfigurationProcessor processor{ CreateConfigurationSetProcessorFactory(context)}; // Set the processor to the current level of the logging. processor.MinimumLevel(ConvertLevel(Logging::Log().GetLevel())); diff --git a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs @@ -15,7 +15,7 @@ namespace AppInstallerCLIE2ETests /// </summary> public class ConfigureCommand { - private const string CommandAndAgreements = "configure --accept-configuration-agreements"; + private const string CommandAndAgreementsAndVerbose = "configure --accept-configuration-agreements --verbose"; /// <summary> /// Setup done once before all the tests here. @@ -45,7 +45,7 @@ namespace AppInstallerCLIE2ETests { TestCommon.EnsureModuleState(Constants.GalleryTestModuleName, present: false); - var result = TestCommon.RunAICLICommand(CommandAndAgreements, TestCommon.GetTestDataFile("Configuration\\PSGallery_NoModule_NoSettings.yml"), timeOut: 120000); + var result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\PSGallery_NoModule_NoSettings.yml"), timeOut: 120000); Assert.AreEqual(Constants.ErrorCode.CONFIG_ERROR_SET_APPLY_FAILED, result.ExitCode); Assert.True(result.StdOut.Contains("The configuration unit failed while attempting to test the current system state.")); } @@ -58,13 +58,57 @@ namespace AppInstallerCLIE2ETests { TestCommon.EnsureModuleState(Constants.SimpleTestModuleName, present: false); - var result = TestCommon.RunAICLICommand(CommandAndAgreements, TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.yml")); + var result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.yml")); Assert.AreEqual(0, result.ExitCode); // The configuration creates a file next to itself with the given contents string targetFilePath = TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.txt"); FileAssert.Exists(targetFilePath); Assert.AreEqual("Contents!", System.IO.File.ReadAllText(targetFilePath)); + + Assert.True(Directory.Exists( + Path.Combine( + TestCommon.GetExpectedModulePath(TestCommon.TestModuleLocation.Default), + Constants.SimpleTestModuleName))); + } + + /// <summary> + /// Simple test to confirm that the module was installed in the right location. + /// </summary> + /// <param name="location">Location to pass.</param> + [TestCase(TestCommon.TestModuleLocation.CurrentUser)] + [TestCase(TestCommon.TestModuleLocation.AllUsers)] + [TestCase(TestCommon.TestModuleLocation.WinGetModulePath)] + [TestCase(TestCommon.TestModuleLocation.Custom)] + [TestCase(TestCommon.TestModuleLocation.Default)] + public void ConfigureFromTestRepo_Location(TestCommon.TestModuleLocation location) + { + TestCommon.EnsureModuleState(Constants.SimpleTestModuleName, present: false); + + string args = TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo_Location.yml"); + if (location == TestCommon.TestModuleLocation.CurrentUser) + { + args += " --module-path currentuser"; + } + else if (location == TestCommon.TestModuleLocation.AllUsers) + { + args += " --module-path allusers"; + } + else if (location == TestCommon.TestModuleLocation.Default) + { + args += " --module-path default"; + } + else if (location == TestCommon.TestModuleLocation.Custom) + { + args += " --module-path " + TestCommon.GetExpectedModulePath(location); + } + + var result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, args); + Assert.AreEqual(0, result.ExitCode); + + Assert.True(Directory.Exists(Path.Combine( + TestCommon.GetExpectedModulePath(location), + Constants.SimpleTestModuleName))); } /// <summary> @@ -73,7 +117,7 @@ namespace AppInstallerCLIE2ETests [Test] public void IndependentResourceWithSingleFailure() { - var result = TestCommon.RunAICLICommand(CommandAndAgreements, TestCommon.GetTestDataFile("Configuration\\IndependentResources_OneFailure.yml")); + var result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\IndependentResources_OneFailure.yml")); Assert.AreEqual(Constants.ErrorCode.CONFIG_ERROR_SET_APPLY_FAILED, result.ExitCode); // The configuration creates a file next to itself with the given contents @@ -88,7 +132,7 @@ namespace AppInstallerCLIE2ETests [Test] public void DependentResourceWithFailure() { - var result = TestCommon.RunAICLICommand(CommandAndAgreements, TestCommon.GetTestDataFile("Configuration\\DependentResources_Failure.yml")); + var result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\DependentResources_Failure.yml")); Assert.AreEqual(Constants.ErrorCode.CONFIG_ERROR_SET_APPLY_FAILED, result.ExitCode); // The configuration creates a file next to itself with the given contents @@ -103,7 +147,7 @@ namespace AppInstallerCLIE2ETests [Test] public void ConfigServerUnexpectedExit() { - var result = TestCommon.RunAICLICommand(CommandAndAgreements, TestCommon.GetTestDataFile("Configuration\\ConfigServerUnexpectedExit.yml")); + var result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\ConfigServerUnexpectedExit.yml")); Assert.AreEqual(Constants.ErrorCode.CONFIG_ERROR_SET_APPLY_FAILED, result.ExitCode); // The configuration creates a file next to itself with the given contents diff --git a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs @@ -52,12 +52,22 @@ namespace AppInstallerCLIE2ETests /// <summary> /// Simple test to confirm that a resource that is already locally available shows that way. /// </summary> - [Test] - public void ShowDetailsFromLocal() + /// <param name="location">The location the module should be before running.</param> + [TestCase(TestCommon.TestModuleLocation.CurrentUser)] + [TestCase(TestCommon.TestModuleLocation.AllUsers)] + [TestCase(TestCommon.TestModuleLocation.WinGetModulePath)] + [TestCase(TestCommon.TestModuleLocation.Custom)] + public void ShowDetailsFromLocal(TestCommon.TestModuleLocation location) { - TestCommon.EnsureModuleState(Constants.SimpleTestModuleName, present: true, repository: Constants.TestRepoName); + TestCommon.EnsureModuleState(Constants.SimpleTestModuleName, present: true, repository: Constants.TestRepoName, location: location); - var result = TestCommon.RunAICLICommand("configure show", $"{TestCommon.GetTestDataFile("Configuration\\ShowDetails_TestRepo.yml")} --verbose"); + string args = $"{TestCommon.GetTestDataFile("Configuration\\ShowDetails_TestRepo.yml")} --verbose"; + if (location == TestCommon.TestModuleLocation.Custom) + { + args += " --module-path " + TestCommon.GetExpectedModulePath(location); + } + + var result = TestCommon.RunAICLICommand("configure show", args); Assert.AreEqual(0, result.ExitCode); Assert.True(result.StdOut.Contains(Constants.LocalModuleDescriptor)); } diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -7,11 +7,16 @@ namespace AppInstallerCLIE2ETests.Helpers { using System; + using System.Collections.Generic; using System.Diagnostics; using System.IO; + using System.Linq; + using System.Management.Automation; using System.Reflection; using System.Threading; + using System.Xml.Linq; using AppInstallerCLIE2ETests; + using AppInstallerCLIE2ETests.PowerShell; using Microsoft.Management.Deployment; using Microsoft.Win32; using NUnit.Framework; @@ -43,6 +48,37 @@ namespace AppInstallerCLIE2ETests.Helpers } /// <summary> + /// The type of location. + /// </summary> + public enum TestModuleLocation + { + /// <summary> + /// Current user. + /// </summary> + CurrentUser, + + /// <summary> + /// All users. + /// </summary> + AllUsers, + + /// <summary> + /// Winget module path. + /// </summary> + WinGetModulePath, + + /// <summary> + /// Custom. + /// </summary> + Custom, + + /// <summary> + /// Default winget configure. + /// </summary> + Default, + } + + /// <summary> /// Run winget command. /// </summary> /// <param name="command">Command to run.</param> @@ -809,24 +845,89 @@ namespace AppInstallerCLIE2ETests.Helpers /// <param name="moduleName">The module.</param> /// <param name="present">Whether the module is present or not.</param> /// <param name="repository">The repository to get the module from if needed.</param> - public static void EnsureModuleState(string moduleName, bool present, string repository = null) + /// <param name="location">The location to install the module.</param> + public static void EnsureModuleState(string moduleName, bool present, string repository = null, TestCommon.TestModuleLocation location = TestModuleLocation.CurrentUser) { - var result = RunCommandWithResult("pwsh", $"-Command \"Get-Module {moduleName} -ListAvailable\""); - bool isPresent = !string.IsNullOrWhiteSpace(result.StdOut); + string wingetModulePath = TestCommon.GetExpectedModulePath(TestModuleLocation.WinGetModulePath); + string customPath = TestCommon.GetExpectedModulePath(TestModuleLocation.Custom); + + ICollection<PSModuleInfo> e2eModule; + bool isPresent = false; + { + using var pwsh = new PowerShellHost(false); + pwsh.AddModulePath($"{wingetModulePath};{customPath}"); - if (isPresent && !present) + e2eModule = pwsh.PowerShell.AddCommand("Get-Module").AddParameter("Name", moduleName).AddParameter("ListAvailable").Invoke<PSModuleInfo>(); + isPresent = e2eModule.Any(); + } + + if (isPresent) { - RunCommand("pwsh", $"-Command \"Uninstall-Module {moduleName}\""); + // If the module was saved in a different location we can't Uninstall-Module. + foreach (var module in e2eModule) + { + var moduleBase = module.Path; + while (Path.GetFileName(moduleBase) != moduleName) + { + moduleBase = Path.GetDirectoryName(moduleBase); + } + + if (!present) + { + Directory.Delete(moduleBase, true); + } + else + { + // Must be present in the right location. + var expectedLocation = TestCommon.GetExpectedModulePath(location); + if (!moduleBase.StartsWith(expectedLocation)) + { + Directory.Delete(moduleBase, true); + isPresent = false; + } + } + } } - else if (!isPresent && present) + + if (!isPresent && present) { - if (string.IsNullOrEmpty(repository)) + if (location == TestModuleLocation.CurrentUser || + location == TestModuleLocation.AllUsers) { - RunCommand("pwsh", $"-Command \"Install-Module {moduleName} -Force\""); + using var pwsh = new PowerShellHost(false); + pwsh.AddModulePath($"{wingetModulePath};{customPath}"); + pwsh.PowerShell.AddCommand("Install-Module").AddParameter("Name", moduleName).AddParameter("Force"); + + if (!string.IsNullOrEmpty(repository)) + { + pwsh.PowerShell.AddParameter("Repository", repository); + } + + if (location == TestModuleLocation.AllUsers) + { + pwsh.PowerShell.AddParameter("Scope", "AllUsers"); + } + + _ = pwsh.PowerShell.Invoke(); } else { - RunCommand("pwsh", $"-Command \"Install-Module {moduleName} -Repository {repository} -Force\""); + string path = customPath; + if (location == TestModuleLocation.WinGetModulePath) + { + path = wingetModulePath; + } + + using var pwsh = new PowerShellHost(false); + pwsh.AddModulePath($"{wingetModulePath};{customPath}"); + pwsh.PowerShell.AddCommand("Save-Module").AddParameter("Name", moduleName).AddParameter("Path", path).AddParameter("Force"); + + if (!string.IsNullOrEmpty(repository)) + { + pwsh.PowerShell.AddParameter("Repository", repository); + } + + _ = pwsh.PowerShell.Invoke(); } } } @@ -900,6 +1001,29 @@ namespace AppInstallerCLIE2ETests.Helpers } /// <summary> + /// Gets the expected module path. + /// </summary> + /// <param name="location">Location.</param> + /// <returns>The expected path of the module.</returns> + public static string GetExpectedModulePath(TestModuleLocation location) + { + switch (location) + { + case TestModuleLocation.CurrentUser: + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), @"PowerShell\Modules"); + case TestModuleLocation.AllUsers: + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"PowerShell\Modules"); + case TestModuleLocation.WinGetModulePath: + case TestModuleLocation.Default: + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Microsoft\WinGet\Configuration\Modules"); + case TestModuleLocation.Custom: + return Path.Combine(Path.GetTempPath(), "E2ECustomModules"); + default: + throw new ArgumentException(location.ToString()); + } + } + + /// <summary> /// Run command result. /// </summary> public struct RunCommandResult diff --git a/src/AppInstallerCLIE2ETests/PowerShell/PowerShellHost.cs b/src/AppInstallerCLIE2ETests/PowerShell/PowerShellHost.cs @@ -26,14 +26,19 @@ namespace AppInstallerCLIE2ETests.PowerShell /// <summary> /// Initializes a new instance of the <see cref="PowerShellHost"/> class. /// </summary> - public PowerShellHost() + /// <param name="importModule">If to import modules.</param> + public PowerShellHost(bool importModule = true) { InitialSessionState initialSessionState = InitialSessionState.CreateDefault(); initialSessionState.ExecutionPolicy = ExecutionPolicy.Unrestricted; - initialSessionState.ImportPSModule(new string[] + + if (importModule) { - TestSetup.Parameters.PowerShellModuleManifestPath, - }); + initialSessionState.ImportPSModule(new string[] + { + TestSetup.Parameters.PowerShellModuleManifestPath, + }); + } this.runspace = RunspaceFactory.CreateRunspace(initialSessionState); this.runspace.Open(); @@ -62,6 +67,16 @@ namespace AppInstallerCLIE2ETests.PowerShell } /// <summary> + /// Add module path. + /// </summary> + /// <param name="path">Path.</param> + public void AddModulePath(string path) + { + var newModulePath = this.PowerShell.Runspace.SessionStateProxy.PSVariable.GetValue("env:PSModulePath") + $";{path}"; + this.PowerShell.Runspace.SessionStateProxy.PSVariable.Set("env:PSModulePath", newModulePath); + } + + /// <summary> /// Protected implementation of dispose pattern. /// </summary> /// <param name="disposing">Dispose.</param> diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/Configure_TestRepo_Location.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/Configure_TestRepo_Location.yml @@ -0,0 +1,8 @@ +properties: + configurationVersion: 0.2 + assertions: + - resource: xE2ETestResource/E2ETestResource + directives: + repository: AppInstallerCLIE2ETestsRepo + settings: + SecretCode: 4815162342+ \ No newline at end of file diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2062,4 +2062,16 @@ Please specify one of them using the --source option to proceed.</value> <data name="InstallerDownloadCommandProhibited" xml:space="preserve"> <value>Installer is prohibited from being downloaded for later offline installation. </value> </data> + <data name="ConfigurationAllUsersElevated" xml:space="preserve"> + <value>`--module-path allusers` requires administrator privileges to execute.</value> + <comment>{Locked="--module-path allusers"}</comment> + </data> + <data name="ConfigurationModulePath" xml:space="preserve"> + <value>Specifies the location on the local computer to store modules. Default %LOCALAPPDATA%\Microsoft\WinGet\Configuration\Modules</value> + <comment>{Locked="allusers","currentuser","default","%LOCALAPPDATA%\Microsoft\WinGet\Configuration\Modules"}</comment> + </data> + <data name="ConfigurationModulePathArgError" xml:space="preserve"> + <value>`--module-path` value must be `currentuser`, `allusers`, `default` or an absolute path.</value> + <comment>{Locked="{--module-path}, {currentuser}, {allusers}, {default}}</comment> + </data> </root> \ No newline at end of file diff --git a/src/Microsoft.Management.Configuration.Processor/Constants/PowerShellConstants.cs b/src/Microsoft.Management.Configuration.Processor/Constants/PowerShellConstants.cs @@ -67,6 +67,7 @@ namespace Microsoft.Management.Configuration.Processor.Constants public const string Recurse = "Recurse"; public const string Repository = "Repository"; public const string RequiredVersion = "RequiredVersion"; + public const string Scope = "Scope"; } internal static class DscMethods diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/IPowerShellGet.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/IPowerShellGet.cs @@ -79,13 +79,15 @@ namespace Microsoft.Management.Configuration.Processor.Helpers /// </summary> /// <param name="pwsh">PowerShell instance.</param> /// <param name="inputObject">Input object.</param> - void InstallModule(PowerShell pwsh, PSObject inputObject); + /// <param name="allUsers">If to install to all users.</param> + void InstallModule(PowerShell pwsh, PSObject inputObject, bool allUsers); /// <summary> /// Calls Install-Module with a module specification. /// </summary> /// <param name="pwsh">PowerShell instance.</param> /// <param name="moduleSpecification">Module specification.</param> - void InstallModule(PowerShell pwsh, ModuleSpecification moduleSpecification); + /// <param name="allUsers">If to install to all users.</param> + void InstallModule(PowerShell pwsh, ModuleSpecification moduleSpecification, bool allUsers); } } diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/PowerShellGetV2.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/PowerShellGetV2.cs @@ -17,6 +17,8 @@ namespace Microsoft.Management.Configuration.Processor.Helpers /// </summary> internal class PowerShellGetV2 : IPowerShellGet { + private const string AllUsers = "AllUsers"; + /// <inheritdoc/> public PSObject? FindModule( PowerShell pwsh, @@ -181,8 +183,19 @@ namespace Microsoft.Management.Configuration.Processor.Helpers /// <inheritdoc/> public void InstallModule( PowerShell pwsh, - PSObject inputObject) + PSObject inputObject, + bool allUsers) { + var parameters = new Dictionary<string, object>() + { + { Parameters.InputObject, inputObject }, + }; + + if (allUsers) + { + parameters.Add(Parameters.Scope, AllUsers); + } + // If the repository is untrusted, it will fail with: // Microsoft.PowerShell.Commands.WriteErrorException : Exception calling "ShouldContinue" with "5" // argument(s): "A command that prompts the user failed because the host program or the command type @@ -191,7 +204,7 @@ namespace Microsoft.Management.Configuration.Processor.Helpers // TODO: Once we have policies, we should remove Force. For hosted environments and depending // on the policy we will trust PSGallery when we create the Runspace or add Force here. _ = pwsh.AddCommand(Commands.InstallModule) - .AddParameter(Parameters.InputObject, inputObject) + .AddParameters(parameters) .AddParameter(Parameters.Force) .Invoke(); } @@ -199,12 +212,14 @@ namespace Microsoft.Management.Configuration.Processor.Helpers /// <inheritdoc/> public void InstallModule( PowerShell pwsh, - ModuleSpecification moduleSpecification) + ModuleSpecification moduleSpecification, + bool allUsers) { var parameters = new Dictionary<string, object>() - { - { Parameters.Name, moduleSpecification.Name }, - }; + { + { Parameters.Name, moduleSpecification.Name }, + }; + if (moduleSpecification.Version is not null) { parameters.Add(Parameters.MinimumVersion, moduleSpecification.Version); @@ -220,6 +235,11 @@ namespace Microsoft.Management.Configuration.Processor.Helpers parameters.Add(Parameters.RequiredVersion, moduleSpecification.RequiredVersion); } + if (allUsers) + { + parameters.Add(Parameters.Scope, AllUsers); + } + _ = pwsh.AddCommand(Commands.InstallModule) .AddParameters(parameters) .AddParameter(Parameters.Force) diff --git a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs @@ -33,6 +33,9 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces private readonly PowerShellConfigurationProcessorType type; private readonly IPowerShellGet powerShellGet; + private PowerShellConfigurationProcessorLocation location = PowerShellConfigurationProcessorLocation.CurrentUser; + private string? customLocation; + /// <summary> /// Initializes a new instance of the <see cref="HostedEnvironment"/> class. /// </summary> @@ -326,9 +329,21 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces /// <inheritdoc/> public void InstallModule(PSObject inputObject) { - using PowerShell pwsh = PowerShell.Create(this.Runspace); - this.powerShellGet.InstallModule(pwsh, inputObject); - this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); + if (this.location == PowerShellConfigurationProcessorLocation.Custom) + { + if (string.IsNullOrEmpty(this.customLocation)) + { + throw new ArgumentNullException(nameof(this.customLocation)); + } + + this.SaveModule(inputObject, this.customLocation); + } + else + { + using PowerShell pwsh = PowerShell.Create(this.Runspace); + this.powerShellGet.InstallModule(pwsh, inputObject, this.location == PowerShellConfigurationProcessorLocation.AllUsers); + this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); + } } /// <inheritdoc/> @@ -338,9 +353,21 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces if (!this.ValidateModule(moduleSpecification)) { // Ok, we have to get it. - using PowerShell pwsh = PowerShell.Create(this.Runspace); - this.powerShellGet.InstallModule(pwsh, moduleSpecification); - this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); + if (this.location == PowerShellConfigurationProcessorLocation.Custom) + { + if (string.IsNullOrEmpty(this.customLocation)) + { + throw new ArgumentNullException(nameof(this.customLocation)); + } + + this.SaveModule(moduleSpecification, this.customLocation); + } + else + { + using PowerShell pwsh = PowerShell.Create(this.Runspace); + this.powerShellGet.InstallModule(pwsh, moduleSpecification, this.location == PowerShellConfigurationProcessorLocation.AllUsers); + this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); + } } } @@ -399,29 +426,59 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces /// <inheritdoc/> public void PrependPSModulePath(string path) { - string oldModulePath = this.GetVariable<string>(Variables.PSModulePath); - this.SetPSModulePath($"{path};{oldModulePath}"); + var oldModulePath = this.GetModulePaths(); + if (!oldModulePath.Contains(path)) + { + this.SetPSModulePath($"{path};{string.Join(";", oldModulePath)}"); + } } /// <inheritdoc/> public void PrependPSModulePaths(IReadOnlyList<string> paths) { - string oldModulePath = this.GetVariable<string>(Variables.PSModulePath); - this.SetPSModulePath($"{string.Join(";", paths)};{oldModulePath}"); + var newPaths = paths.ToList(); + var oldModulePath = this.GetModulePaths(); + foreach (var newPath in paths) + { + if (oldModulePath.Contains(newPath)) + { + newPaths.Remove(newPath); + } + } + + if (newPaths.Any()) + { + this.SetPSModulePath($"{string.Join(";", newPaths)};{string.Join(";", oldModulePath)}"); + } } /// <inheritdoc/> public void AppendPSModulePath(string path) { - string oldModulePath = this.GetVariable<string>(Variables.PSModulePath); - this.SetPSModulePath($"{oldModulePath};{path}"); + var oldModulePath = this.GetModulePaths(); + if (!oldModulePath.Contains(path)) + { + this.SetPSModulePath($"{string.Join(";", oldModulePath)};{path}"); + } } /// <inheritdoc/> public void AppendPSModulePaths(IReadOnlyList<string> paths) { - string oldModulePath = this.GetVariable<string>(Variables.PSModulePath); - this.SetPSModulePath($"{oldModulePath};{string.Join(";", paths)}"); + var newPaths = paths.ToList(); + var oldModulePath = this.GetModulePaths(); + foreach (var newPath in paths) + { + if (oldModulePath.Contains(newPath)) + { + newPaths.Remove(newPath); + } + } + + if (newPaths.Any()) + { + this.SetPSModulePath($"{string.Join(";", oldModulePath)};{string.Join(";", newPaths)}"); + } } /// <inheritdoc/> @@ -434,6 +491,21 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces this.SetPSModulePath(newModulePath); } + /// <inheritdoc/> + public void SetLocation(PowerShellConfigurationProcessorLocation location, string? customLocation) + { + this.location = location; + if (this.location == PowerShellConfigurationProcessorLocation.Custom) + { + if (string.IsNullOrEmpty(customLocation)) + { + throw new ArgumentNullException(nameof(customLocation)); + } + + this.customLocation = customLocation; + } + } + private bool ValidateModule(ModuleSpecification moduleSpecification) { var loadedModule = this.GetImportedModule(moduleSpecification); @@ -461,5 +533,10 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces { this.SetProcessorFactory?.OnDiagnostics(level, message); } + + private HashSet<string> GetModulePaths() + { + return this.GetVariable<string>(Variables.PSModulePath).Split(";").ToHashSet<string>(); + } } } diff --git a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/IProcessorEnvironment.cs b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/IProcessorEnvironment.cs @@ -221,5 +221,12 @@ namespace Microsoft.Management.Configuration.Processor.ProcessorEnvironments /// </summary> /// <param name="path">Path.</param> void CleanupPSModulePath(string path); + + /// <summary> + /// Sets the location for installing modules. + /// </summary> + /// <param name="location">Location.</param> + /// <param name="customLocation">Path for custom location.</param> + void SetLocation(PowerShellConfigurationProcessorLocation location, string? customLocation); } } diff --git a/src/Microsoft.Management.Configuration.Processor/Public/IPowerShellConfigurationProcessorFactoryProperties.cs b/src/Microsoft.Management.Configuration.Processor/Public/IPowerShellConfigurationProcessorFactoryProperties.cs @@ -7,7 +7,6 @@ namespace Microsoft.Management.Configuration.Processor { using System.Collections.Generic; - using System.Runtime.InteropServices; /// <summary> /// Properties for the configuration processor factory. @@ -28,5 +27,15 @@ namespace Microsoft.Management.Configuration.Processor /// Gets or sets the configuration policy. /// </summary> PowerShellConfigurationProcessorPolicy Policy { get; set; } + + /// <summary> + /// Gets or sets the module location. + /// </summary> + PowerShellConfigurationProcessorLocation Location { get; set; } + + /// <summary> + /// Gets or sets the install module path. Only used for Scope.Custom. + /// </summary> + string? CustomLocation { get; set; } } } diff --git a/src/Microsoft.Management.Configuration.Processor/Public/PowerShellConfigurationProcessorLocation.cs b/src/Microsoft.Management.Configuration.Processor/Public/PowerShellConfigurationProcessorLocation.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PowerShellConfigurationProcessorLocation.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor +{ + /// <summary> + /// The location where modules are going to be installed. + /// </summary> + public enum PowerShellConfigurationProcessorLocation + { + /// <summary> + /// Current user path. + /// </summary> + CurrentUser = 0, + + /// <summary> + /// AllUsers path. Requires admin. + /// </summary> + AllUsers = 1, + + /// <summary> + /// The winget location %LOCALAPPDATA%\Microsoft\WinGet\Configuration\Modules. + /// </summary> + WinGetModulePath = 2, + + /// <summary> + /// Custom path. + /// </summary> + Custom = 3, + + /// <summary> + /// Default. + /// </summary> + Default = WinGetModulePath, + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Public/PowerShellConfigurationSetProcessorFactory.cs b/src/Microsoft.Management.Configuration.Processor/Public/PowerShellConfigurationSetProcessorFactory.cs @@ -8,6 +8,7 @@ namespace Microsoft.Management.Configuration.Processor { using System; using System.Collections.Generic; + using System.IO; using System.Management.Automation; using System.Text; using Microsoft.Management.Configuration; @@ -53,6 +54,16 @@ namespace Microsoft.Management.Configuration.Processor public PowerShellConfigurationProcessorPolicy Policy { get; set; } = PowerShellConfigurationProcessorPolicy.Default; /// <summary> + /// Gets or sets the module scope. + /// </summary> + public PowerShellConfigurationProcessorLocation Location { get; set; } = PowerShellConfigurationProcessorLocation.Default; + + /// <summary> + /// Gets or sets the install module path. Only used for Scope = Custom. + /// </summary> + public string? CustomLocation { get; set; } + + /// <summary> /// Gets the configuration unit processor details for the given unit. /// </summary> /// <param name="set">Configuration Set.</param> @@ -73,6 +84,29 @@ namespace Microsoft.Management.Configuration.Processor processorEnvironment.PrependPSModulePaths(this.AdditionalModulePaths); } + // Always add the winget path. + var wingetModulePath = GetWinGetModulePath(); + processorEnvironment.PrependPSModulePath(wingetModulePath); + if (this.Location == PowerShellConfigurationProcessorLocation.WinGetModulePath) + { + this.OnDiagnostics(DiagnosticLevel.Verbose, "Using winget module path"); + processorEnvironment.SetLocation(PowerShellConfigurationProcessorLocation.Custom, wingetModulePath); + } + else if (this.Location == PowerShellConfigurationProcessorLocation.Custom) + { + if (string.IsNullOrEmpty(this.CustomLocation)) + { + throw new ArgumentNullException(nameof(this.CustomLocation)); + } + + processorEnvironment.SetLocation(this.Location, this.CustomLocation); + processorEnvironment.PrependPSModulePath(this.CustomLocation); + } + else + { + processorEnvironment.SetLocation(this.Location, null); + } + this.OnDiagnostics(DiagnosticLevel.Verbose, $" Effective module path:\n{processorEnvironment.GetVariable<string>(Variables.PSModulePath)}"); processorEnvironment.ValidateRunspace(); @@ -89,6 +123,17 @@ namespace Microsoft.Management.Configuration.Processor } /// <summary> + /// Gets the winget module path. + /// </summary> + /// <returns>The winget module path.</returns> + internal static string GetWinGetModulePath() + { + return Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + @"Microsoft\WinGet\Configuration\Modules"); + } + + /// <summary> /// Sends diagnostics if appropriate. /// </summary> /// <param name="level">The level of this diagnostic message.</param> diff --git a/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs b/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs @@ -64,6 +64,7 @@ namespace Microsoft.Management.Configuration.Processor.Set var dscResourceInfo = this.PrepareUnitForProcessing(configurationUnitInternal); this.OnDiagnostics(DiagnosticLevel.Verbose, "... done creating unit processor."); + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Using unit from location: {dscResourceInfo.Path}"); return new ConfigurationUnitProcessor( this.ProcessorEnvironment, new ConfigurationUnitAndResource(configurationUnitInternal, dscResourceInfo)) diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorFactoryTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorFactoryTests.cs @@ -82,7 +82,58 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.NotNull(processorSet); var modulePath = processorSet.ProcessorEnvironment.GetVariable<string>(Variables.PSModulePath); - Assert.StartsWith("ThisIsOnePath;ThisIsAnotherPath", modulePath); + Assert.Contains("ThisIsOnePath;ThisIsAnotherPath", modulePath); + } + + /// <summary> + /// Make sure the winget path is always added to PSModulePath. + /// </summary> + /// <param name="location">Location.</param> + [Theory] + [InlineData(PowerShellConfigurationProcessorLocation.CurrentUser)] + [InlineData(PowerShellConfigurationProcessorLocation.AllUsers)] + [InlineData(PowerShellConfigurationProcessorLocation.WinGetModulePath)] + [InlineData(PowerShellConfigurationProcessorLocation.Custom)] + public void CreateSetProcessor_WinGetPath(PowerShellConfigurationProcessorLocation location) + { + var configurationProcessorFactory = new PowerShellConfigurationSetProcessorFactory(); + + var properties = configurationProcessorFactory.As<IPowerShellConfigurationProcessorFactoryProperties>(); + properties.Location = location; + + if (properties.Location == PowerShellConfigurationProcessorLocation.Custom) + { + properties.CustomLocation = @"c:\this\is\a\module\path"; + } + + var configurationSet = new ConfigurationSet(); + + var setProcessor = configurationProcessorFactory.CreateSetProcessor(configurationSet) as ConfigurationSetProcessor; + Assert.NotNull(setProcessor); + + var modulePath = setProcessor.ProcessorEnvironment.GetVariable<string>(Variables.PSModulePath); + Assert.Contains($"{PowerShellConfigurationSetProcessorFactory.GetWinGetModulePath()};", modulePath); + } + + /// <summary> + /// Tests the custom location is added successfully. + /// </summary> + [Fact] + public void CreateSetProcessor_CustomLocation() + { + var configurationProcessorFactory = new PowerShellConfigurationSetProcessorFactory(); + + var properties = configurationProcessorFactory.As<IPowerShellConfigurationProcessorFactoryProperties>(); + properties.Location = PowerShellConfigurationProcessorLocation.Custom; + properties.CustomLocation = @"c:\this\is\a\module\path"; + + var configurationSet = new ConfigurationSet(); + + var setProcessor = configurationProcessorFactory.CreateSetProcessor(configurationSet) as ConfigurationSetProcessor; + Assert.NotNull(setProcessor); + + var modulePath = setProcessor.ProcessorEnvironment.GetVariable<string>(Variables.PSModulePath); + Assert.Contains($"{properties.CustomLocation};", modulePath); } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ProcessorEnvironmentTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ProcessorEnvironmentTests.cs @@ -6,14 +6,9 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests { - using System; using System.Collections.Generic; - using System.Linq; - using System.Management.Automation; - using Microsoft.Management.Configuration.Processor.ProcessorEnvironments; using Microsoft.Management.Configuration.UnitTests.Fixtures; using Moq; - using Windows.Security.Authentication.OnlineId; using Xunit; using Xunit.Abstractions; using static Microsoft.Management.Configuration.Processor.Constants.PowerShellConstants; @@ -120,6 +115,12 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests processorEnv.AppendPSModulePath(psModulePathInput); psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); Assert.EndsWith($";{psModulePathInput}", psModulePath); + + // No duplicates + processorEnv.AppendPSModulePath(psModulePathInput); + psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.False(psModulePath.EndsWith($";{psModulePathInput};{psModulePathInput}")); + Assert.EndsWith($";{psModulePathInput}", psModulePath); } /// <summary> @@ -145,6 +146,17 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests processorEnv.AppendPSModulePaths(psModulePathInput); psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); Assert.EndsWith($";{psModulePathExpected}", psModulePath); + + // No duplicates + psModulePathInput = new List<string>() + { + "AppendPSModulePathsPath1", + "AppendPSModulePathsPath5", + "AppendPSModulePathsPath2", + }; + processorEnv.AppendPSModulePaths(psModulePathInput); + psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.EndsWith($";{psModulePathExpected};AppendPSModulePathsPath5", psModulePath); } /// <summary> @@ -162,6 +174,12 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests processorEnv.PrependPSModulePath(psModulePathInput); psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); Assert.StartsWith($"{psModulePathInput};", psModulePath); + + // No duplicates + processorEnv.PrependPSModulePath(psModulePathInput); + psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.False(psModulePath.StartsWith($"{psModulePathInput};{psModulePathInput};")); + Assert.StartsWith($"{psModulePathInput};", psModulePath); } /// <summary> @@ -187,6 +205,17 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests processorEnv.PrependPSModulePaths(psModulePathInput); psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); Assert.StartsWith($"{psModulePathExpected};", psModulePath); + + // No duplicates + psModulePathInput = new List<string>() + { + "PrependPSModulePathsPath1", + "PrependPSModulePathsPath5", + "PrependPSModulePathsPath2", + }; + processorEnv.PrependPSModulePaths(psModulePathInput); + psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.StartsWith($"PrependPSModulePathsPath5;{psModulePathExpected};", psModulePath); } /// <summary> diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.idl b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.idl @@ -757,6 +757,15 @@ namespace Microsoft.Management.Configuration Default = RemoteSigned, }; + enum PwshConfigurationProcessorLocation + { + CurrentUser = 0, + AllUsers = 1, + WinGetModulePath = 2, + Custom = 3, + Default = WinGetModulePath, + }; + // The properties provided by the "pwsh" processor factory. interface IPwshConfigurationSetProcessorFactoryProperties { @@ -766,6 +775,12 @@ namespace Microsoft.Management.Configuration // The execution policy to apply; must be set before taking actions with the processor. PwshConfigurationProcessorPolicy Policy; + + // The location to install modules. Default is WinGetModulePath + PwshConfigurationProcessorLocation Location; + + // The custom location. Only applicable for Scope.Custom + String CustomLocation; }; } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs @@ -0,0 +1,54 @@ +// ----------------------------------------------------------------------------- +// <copyright file="OpenConfiguration.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets.Common +{ + using System.Management.Automation; + using Microsoft.PowerShell; + using Microsoft.WinGet.Configuration.Helpers; + + /// <summary> + /// The parameters used to open a configuration. + /// </summary> + public abstract class OpenConfiguration : PSCmdlet + { + /// <summary> + /// Gets or sets the configuration file. + /// </summary> + [Parameter( + Mandatory = true, + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSet.OpenConfigurationSet)] + public string File { get; set; } + + /// <summary> + /// Gets or sets custom location to install modules. + /// </summary> + [Parameter( + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSet.OpenConfigurationSet)] + public string ModulePath { get; set; } + + /// <summary> + /// Gets the execution policy to use. + /// </summary> + protected ExecutionPolicy ExecutionPolicy { get; private set; } = ExecutionPolicy.Undefined; + + /// <summary> + /// Gets a value indicating whether to use telemetry or not. + /// </summary> + protected bool CanUseTelemetry { get; private set; } + + /// <summary> + /// Pre-processing operations. + /// </summary> + protected override void BeginProcessing() + { + this.ExecutionPolicy = Utilities.GetExecutionPolicy(); + this.CanUseTelemetry = Utilities.CanUseTelemetry(); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs @@ -7,44 +7,27 @@ namespace Microsoft.WinGet.Configuration.Cmdlets { using System.Management.Automation; - using Microsoft.PowerShell; + using Microsoft.WinGet.Configuration.Cmdlets.Common; using Microsoft.WinGet.Configuration.Engine.Commands; - using Microsoft.WinGet.Configuration.Helpers; /// <summary> /// Get-WinGetConfiguration. /// Opens a configuration set. /// </summary> [Cmdlet(VerbsCommon.Get, "WinGetConfiguration")] - public sealed class GetWinGetConfigurationCmdlet : PSCmdlet + public sealed class GetWinGetConfigurationCmdlet : OpenConfiguration { - private ExecutionPolicy executionPolicy = ExecutionPolicy.Undefined; - private bool canUseTelemetry = true; - - /// <summary> - /// Gets or sets the configuration file. - /// </summary> - [Parameter( - Mandatory = true, - ValueFromPipelineByPropertyName = true)] - public string File { get; set; } - - /// <summary> - /// Pre-processing operations. - /// </summary> - protected override void BeginProcessing() - { - this.executionPolicy = Utilities.GetExecutionPolicy(); - this.canUseTelemetry = Utilities.CanUseTelemetry(); - } - /// <summary> /// Opens the configuration set. /// </summary> protected override void ProcessRecord() { var configCommand = new ConfigurationCommand(this); - configCommand.Get(this.File, this.executionPolicy, this.canUseTelemetry); + configCommand.Get( + this.File, + this.ModulePath, + this.ExecutionPolicy, + this.CanUseTelemetry); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Constants.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Constants.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------------- +// <copyright file="Constants.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Helpers +{ + /// <summary> + /// Constants. + /// </summary> + internal static class Constants + { +#pragma warning disable SA1600 // ElementsMustBeDocumented + internal static class ParameterSet + { + internal const string OpenConfigurationSet = "OpenConfigurationSet"; + } +#pragma warning restore SA1600 // ElementsMustBeDocumented + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs @@ -74,11 +74,17 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands /// Open a configuration set. /// </summary> /// <param name="configFile">Configuration file path.</param> + /// <param name="modulePath">The module path to use.</param> /// <param name="executionPolicy">Execution policy.</param> /// <param name="canUseTelemetry">If telemetry can be used.</param> - public void Get(string configFile, ExecutionPolicy executionPolicy, bool canUseTelemetry) + public void Get( + string configFile, + string modulePath, + ExecutionPolicy executionPolicy, + bool canUseTelemetry) { - configFile = this.VerifyFile(configFile); + var openParams = new OpenConfigurationParameters( + this.PsCmdlet, configFile, modulePath, executionPolicy, canUseTelemetry); // Start task. var runningTask = this.RunOnMTA<PSConfigurationSet>( @@ -86,7 +92,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { try { - return await this.OpenConfigurationSetAsync(configFile, executionPolicy, canUseTelemetry); + return await this.OpenConfigurationSetAsync(openParams); } finally { @@ -199,29 +205,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands this.ContinueHelper(psConfigurationJob); } - /// <summary> - /// Verifies file exists and return the full path, if not already. - /// </summary> - /// <param name="filePath">File path.</param> - /// <returns>Full path.</returns> - private string VerifyFile(string filePath) - { - if (!Path.IsPathRooted(filePath)) - { - filePath = Path.GetFullPath( - Path.Combine( - this.PsCmdlet.SessionState.Path.CurrentFileSystemLocation.Path, - filePath)); - } - - if (!File.Exists(filePath)) - { - throw new FileNotFoundException(filePath); - } - - return filePath; - } - private void ContinueHelper(PSConfigurationJob psConfigurationJob) { // Signal the command that it can write to streams and wait for task. @@ -230,39 +213,44 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands this.Write(StreamType.Object, psConfigurationJob.ConfigurationTask.Result); } - private PSConfigurationProcessor CreateConfigurationProcessor(ExecutionPolicy executionPolicy, bool canUseTelemetry) + private IConfigurationSetProcessorFactory CreateFactory(OpenConfigurationParameters openParams) { - this.Write(StreamType.Information, Resources.ConfigurationInitializing); - var factory = new PowerShellConfigurationSetProcessorFactory(); var properties = factory.As<IPowerShellConfigurationProcessorFactoryProperties>(); - properties.Policy = this.GetConfigurationProcessorPolicy(executionPolicy); + properties.Policy = openParams.Policy; properties.ProcessorType = PowerShellConfigurationProcessorType.Default; + properties.Location = openParams.Location; + if (properties.Location == PowerShellConfigurationProcessorLocation.Custom) + { + properties.CustomLocation = openParams.CustomLocation; + } - return new PSConfigurationProcessor(factory, this, canUseTelemetry); + return factory; } - private async Task<PSConfigurationSet> OpenConfigurationSetAsync(string configFile, ExecutionPolicy executionPolicy, bool canUseTelemetry) + private async Task<PSConfigurationSet> OpenConfigurationSetAsync(OpenConfigurationParameters openParams) { - var psProcessor = this.CreateConfigurationProcessor(executionPolicy, canUseTelemetry); + this.Write(StreamType.Verbose, Resources.ConfigurationInitializing); - this.Write(StreamType.Information, Resources.ConfigurationReadingConfigFile); - var stream = await FileRandomAccessStream.OpenAsync(configFile, FileAccessMode.Read); + var psProcessor = new PSConfigurationProcessor(this.CreateFactory(openParams), this, openParams.CanUseTelemetry); + + this.Write(StreamType.Verbose, Resources.ConfigurationReadingConfigFile); + var stream = await FileRandomAccessStream.OpenAsync(openParams.ConfigFile, FileAccessMode.Read); OpenConfigurationSetResult openResult = await psProcessor.Processor.OpenConfigurationSetAsync(stream); if (openResult.ResultCode != null) { - throw new OpenConfigurationSetException(openResult, configFile); + throw new OpenConfigurationSetException(openResult, openParams.ConfigFile); } var set = openResult.Set; // This should match winget's OpenConfigurationSet or OpenConfigurationSetAsync // should be modify to take the full path and handle it. - set.Name = Path.GetFileName(configFile); - set.Origin = Path.GetDirectoryName(configFile); - set.Path = configFile; + set.Name = Path.GetFileName(openParams.ConfigFile); + set.Origin = Path.GetDirectoryName(openParams.ConfigFile); + set.Path = openParams.ConfigFile; return new PSConfigurationSet(psProcessor, set); } @@ -367,12 +355,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands if (detailsProgressOutput.UnitsShown == 0) { this.Write(StreamType.Warning, Resources.ConfigurationFailedToGetDetails); - foreach (var unit in set.ConfigurationUnits) - { - var information = new ConfigurationUnitInformation(unit); - this.Write(StreamType.Information, information.GetHeader()); - this.Write(StreamType.Information, information.GetInformation()); - } } else { @@ -394,18 +376,5 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands resultInformation.ResultCode); } } - - private PowerShellConfigurationProcessorPolicy GetConfigurationProcessorPolicy(ExecutionPolicy policy) - { - return policy switch - { - ExecutionPolicy.Unrestricted => PowerShellConfigurationProcessorPolicy.Unrestricted, - ExecutionPolicy.RemoteSigned => PowerShellConfigurationProcessorPolicy.RemoteSigned, - ExecutionPolicy.AllSigned => PowerShellConfigurationProcessorPolicy.AllSigned, - ExecutionPolicy.Restricted => PowerShellConfigurationProcessorPolicy.Restricted, - ExecutionPolicy.Bypass => PowerShellConfigurationProcessorPolicy.Bypass, - _ => throw new InvalidOperationException(), - }; - } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs @@ -28,7 +28,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers private readonly string completeMessage; private readonly int totalUnitsExpected; - private readonly HashSet<Guid> unitsSeen = new (); private readonly HashSet<Guid> unitsCompleted = new (); private bool isFirstProgress = true; @@ -70,15 +69,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers switch (data.Change) { - case ConfigurationSetChangeEventType.SetStateChanged: - switch (data.SetState) - { - case ConfigurationSetState.Pending: - this.cmd.Write(StreamType.Information, Utilities.CreateInformationMessage(Resources.ConfigurationWaitingOnAnother)); - break; - } - - break; case ConfigurationSetChangeEventType.UnitStateChanged: this.HandleUnitProgress(data.Unit, data.UnitState, data.ResultInformation); break; @@ -122,47 +112,15 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers // The unreported progress handler may send pending units, just ignore them break; case ConfigurationUnitState.InProgress: - this.OutputUnitInProgressIfNeeded(unit); break; case ConfigurationUnitState.Completed: - this.OutputUnitInProgressIfNeeded(unit); - if (resultInformation.ResultCode == null) - { - this.cmd.Write(StreamType.Information, Utilities.CreateInformationMessage($" {Resources.ConfigurationSuccessfullyApplied}")); - } - else + if (resultInformation.ResultCode != null) { string description = resultInformation.Description.Trim(); - var (message, showDescription) = this.GetUnitFailedMessage(unit, resultInformation); - var sb = new StringBuilder(); - sb.AppendLine($" {message}"); - - if (showDescription && !string.IsNullOrEmpty(description)) - { - bool wasLimited = false; - const int maxLines = 3; - var lines = Utilities.SplitIntoLines(description, maxLines + 1); - - for (int i = 0; i < lines.Length && i < maxLines; i++) - { - sb.AppendLine(lines[i]); - } - - if (lines.Length > maxLines) - { - wasLimited = true; - } - - if (wasLimited || string.IsNullOrEmpty(resultInformation.Details)) - { - sb.AppendLine(Resources.ConfigurationDescriptionWasTruncated); - } - } - - this.cmd.Write(StreamType.Information, Utilities.CreateInformationMessage(sb.ToString(), foregroundColor: ConsoleColor.DarkRed)); + var message = this.GetUnitFailedMessage(unit, resultInformation); string errorMessage = $"Configuration unit {unit.UnitName}[{unit.Identifier}] failed with code 0x{resultInformation.ResultCode.HResult:X}" + - $" and error message:\n{description}\n{resultInformation.Details}"; + $" and error message:\n{description}\n{resultInformation.Details}\n{message}"; this.cmd.WriteError( ErrorRecordErrorId.ConfigurationApplyError, errorMessage, @@ -172,7 +130,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers this.CompleteUnit(unit); break; case ConfigurationUnitState.Skipped: - this.OutputUnitInProgressIfNeeded(unit); this.cmd.Write(StreamType.Warning, this.GetUnitSkippedMessage(resultInformation)); this.CompleteUnit(unit); break; @@ -187,68 +144,57 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers } } - private void OutputUnitInProgressIfNeeded(ConfigurationUnit unit) - { - var unitInstance = unit.InstanceIdentifier; - if (!this.unitsSeen.Contains(unitInstance)) - { - this.unitsSeen.Add(unitInstance); - var unitInfo = new ConfigurationUnitInformation(unit); - this.cmd.Write(StreamType.Information, unitInfo.GetHeader()); - } - } - - private (string message, bool showDescription) GetUnitFailedMessage(ConfigurationUnit unit, IConfigurationUnitResultInformation resultInformation) + private string GetUnitFailedMessage(ConfigurationUnit unit, IConfigurationUnitResultInformation resultInformation) { if (resultInformation.ResultCode == null) { - return (string.Format(Resources.ConfigurationUnitFailed, "null"), false); + return string.Format(Resources.ConfigurationUnitFailed, "null"); } int resultCode = resultInformation.ResultCode.HResult; switch (resultCode) { case ErrorCodes.WingetConfigErrorDuplicateIdentifier: - return (string.Format(Resources.ConfigurationUnitHasDuplicateIdentifier, unit.Identifier), false); + return string.Format(Resources.ConfigurationUnitHasDuplicateIdentifier, unit.Identifier); case ErrorCodes.WingetConfigErrorMissingDependency: - return (string.Format(Resources.ConfigurationUnitHasMissingDependency, resultInformation.Details), false); + return string.Format(Resources.ConfigurationUnitHasMissingDependency, resultInformation.Details); case ErrorCodes.WingetConfigErrorAssertionFailed: - return (Resources.ConfigurationUnitAssertHadNegativeResult, false); + return Resources.ConfigurationUnitAssertHadNegativeResult; case ErrorCodes.WinGetConfigUnitNotFound: - return (Resources.ConfigurationUnitNotFoundInModule, false); + return Resources.ConfigurationUnitNotFoundInModule; case ErrorCodes.WinGetConfigUnitNotFoundRepository: - return (Resources.ConfigurationUnitNotFound, false); + return Resources.ConfigurationUnitNotFound; case ErrorCodes.WinGetConfigUnitMultipleMatches: - return (Resources.ConfigurationUnitMultipleMatches, false); + return Resources.ConfigurationUnitMultipleMatches; case ErrorCodes.WinGetConfigUnitInvokeGet: - return (Resources.ConfigurationUnitFailedDuringGet, true); + return Resources.ConfigurationUnitFailedDuringGet; case ErrorCodes.WinGetConfigUnitInvokeTest: - return (Resources.ConfigurationUnitFailedDuringTest, true); + return Resources.ConfigurationUnitFailedDuringTest; case ErrorCodes.WinGetConfigUnitInvokeSet: - return (Resources.ConfigurationUnitFailedDuringSet, true); + return Resources.ConfigurationUnitFailedDuringSet; case ErrorCodes.WinGetConfigUnitModuleConflict: - return (Resources.ConfigurationUnitModuleConflict, false); + return Resources.ConfigurationUnitModuleConflict; case ErrorCodes.WinGetConfigUnitImportModule: - return (Resources.ConfigurationUnitModuleImportFailed, true); + return Resources.ConfigurationUnitModuleImportFailed; case ErrorCodes.WinGetConfigUnitInvokeInvalidResult: - return (Resources.ConfigurationUnitReturnedInvalidResult, false); + return Resources.ConfigurationUnitReturnedInvalidResult; } switch (resultInformation.ResultSource) { case ConfigurationUnitResultSource.ConfigurationSet: - return (string.Format(Resources.ConfigurationUnitFailedConfigSet, resultCode), true); + return string.Format(Resources.ConfigurationUnitFailedConfigSet, resultCode); case ConfigurationUnitResultSource.Internal: - return (string.Format(Resources.ConfigurationUnitFailedInternal, resultCode), true); + return string.Format(Resources.ConfigurationUnitFailedInternal, resultCode); case ConfigurationUnitResultSource.Precondition: - return (string.Format(Resources.ConfigurationUnitFailedPrecondition, resultCode), true); + return string.Format(Resources.ConfigurationUnitFailedPrecondition, resultCode); case ConfigurationUnitResultSource.SystemState: - return (string.Format(Resources.ConfigurationUnitFailedSystemState, resultCode), true); + return string.Format(Resources.ConfigurationUnitFailedSystemState, resultCode); case ConfigurationUnitResultSource.UnitProcessing: - return (string.Format(Resources.ConfigurationUnitFailedUnitProcessing, resultCode), true); + return string.Format(Resources.ConfigurationUnitFailedUnitProcessing, resultCode); } - return (string.Format(Resources.ConfigurationUnitFailed, resultCode), true); + return string.Format(Resources.ConfigurationUnitFailed, resultCode); } private string GetUnitSkippedMessage(IConfigurationUnitResultInformation resultInformation) diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs @@ -74,10 +74,6 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers { GetConfigurationUnitDetailsResult unitResult = unitResults[this.UnitsShown]; this.LogFailedGetConfigurationUnitDetails(unitResult.Unit, unitResult.ResultInformation); - var information = new ConfigurationUnitInformation(unitResult.Unit); - this.cmd.Write(StreamType.Information, information.GetHeader()); - this.cmd.Write(StreamType.Information, information.GetInformation()); - ++this.UnitsShown; this.cmd.WriteProgressWithPercentage(this.activityId, this.activity, $"{this.inProgressMessage} {this.UnitsShown}/{this.totalUnitsExpected}", this.UnitsShown, this.totalUnitsExpected); } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs @@ -0,0 +1,142 @@ +// ----------------------------------------------------------------------------- +// <copyright file="OpenConfigurationParameters.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Helpers +{ + using System; + using System.IO; + using System.Management.Automation; + using Microsoft.Management.Configuration.Processor; + using Microsoft.PowerShell; + using Microsoft.WinGet.Configuration.Engine.Resources; + + /// <summary> + /// The parameters used to open a configuration. + /// </summary> + internal class OpenConfigurationParameters + { + private const string Default = "default"; + private const string AllUsers = "allusers"; + private const string CurrentUser = "currentuser"; + + /// <summary> + /// Initializes a new instance of the <see cref="OpenConfigurationParameters"/> class. + /// </summary> + /// <param name="psCmdlet">PsCmdlet caller.</param> + /// <param name="file">The configuration file.</param> + /// <param name="modulePath">The module path to use.</param> + /// <param name="executionPolicy">Execution policy.</param> + /// <param name="canUseTelemetry">If telemetry can be used.</param> + public OpenConfigurationParameters( + PSCmdlet psCmdlet, + string file, + string modulePath, + ExecutionPolicy executionPolicy, + bool canUseTelemetry) + { + this.ConfigFile = this.VerifyFile(file, psCmdlet); + this.InitializeModulePath(modulePath); + this.Policy = this.GetConfigurationProcessorPolicy(executionPolicy); + this.CanUseTelemetry = canUseTelemetry; + } + + /// <summary> + /// Gets the configuration file. + /// </summary> + public string ConfigFile { get; } + + /// <summary> + /// Gets the location to install the modules. + /// </summary> + public PowerShellConfigurationProcessorLocation Location { get; private set; } + + /// <summary> + /// Gets the custom location to install modules. + /// </summary> + public string? CustomLocation { get; private set; } + + /// <summary> + /// Gets execution policy of the processor. + /// </summary> + public PowerShellConfigurationProcessorPolicy Policy { get; } + + /// <summary> + /// Gets a value indicating whether to use telemetry or not. + /// </summary> + public bool CanUseTelemetry { get; } + + private string VerifyFile(string filePath, PSCmdlet psCmdlet) + { + if (!Path.IsPathRooted(filePath)) + { + filePath = Path.GetFullPath( + Path.Combine( + psCmdlet.SessionState.Path.CurrentFileSystemLocation.Path, + filePath)); + } + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException(filePath); + } + + return filePath; + } + + private PowerShellConfigurationProcessorPolicy GetConfigurationProcessorPolicy(ExecutionPolicy policy) + { + return policy switch + { + ExecutionPolicy.Unrestricted => PowerShellConfigurationProcessorPolicy.Unrestricted, + ExecutionPolicy.RemoteSigned => PowerShellConfigurationProcessorPolicy.RemoteSigned, + ExecutionPolicy.AllSigned => PowerShellConfigurationProcessorPolicy.AllSigned, + ExecutionPolicy.Restricted => PowerShellConfigurationProcessorPolicy.Restricted, + ExecutionPolicy.Bypass => PowerShellConfigurationProcessorPolicy.Bypass, + _ => throw new InvalidOperationException(), + }; + } + + private void InitializeModulePath(string modulePath) + { + // TODO: Create cmdlet that specify the global custom location for a PowerShell session. + if (!string.IsNullOrEmpty(modulePath)) + { + if (string.Compare(modulePath, Default, true) == 0) + { + this.Location = PowerShellConfigurationProcessorLocation.Default; + } + else if (string.Compare(modulePath, CurrentUser, true) == 0) + { + this.Location = PowerShellConfigurationProcessorLocation.CurrentUser; + } + else if (string.Compare(modulePath, AllUsers, true) == 0) + { + if (!Utilities.ExecutingAsAdministrator) + { + throw new ArgumentException(Resources.ConfigurationAllUsersElevated); + } + + this.Location = PowerShellConfigurationProcessorLocation.AllUsers; + } + else + { + string customLocation = modulePath; + if (!Path.IsPathRooted(customLocation)) + { + throw new ArgumentException(Resources.ConfigurationModulePathArgError); + } + + this.CustomLocation = customLocation; + this.Location = PowerShellConfigurationProcessorLocation.Custom; + } + } + else + { + this.Location = PowerShellConfigurationProcessorLocation.WinGetModulePath; + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs @@ -11,6 +11,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers using System.Linq; using System.Management.Automation; using System.Management.Automation.Host; + using System.Security.Principal; /// <summary> /// Helper methods. @@ -18,6 +19,20 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers internal static class Utilities { /// <summary> + /// Gets a value indicating whether the current assembly is executing in an administrative context. + /// </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Windows only API")] + public static bool ExecutingAsAdministrator + { + get + { + WindowsIdentity identity = WindowsIdentity.GetCurrent(); + WindowsPrincipal principal = new (identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + } + + /// <summary> /// Helper for StreamType.Information. Creates a HostInformationMessage with /// the specified information. /// </summary> diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs @@ -70,6 +70,15 @@ namespace Microsoft.WinGet.Configuration.Engine.Resources { } /// <summary> + /// Looks up a localized string similar to `-ModulePath AllUsers` require administrator privileges to execute.. + /// </summary> + internal static string ConfigurationAllUsersElevated { + get { + return ResourceManager.GetString("ConfigurationAllUsersElevated", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Apply. /// </summary> internal static string ConfigurationApply { @@ -232,6 +241,15 @@ namespace Microsoft.WinGet.Configuration.Engine.Resources { } /// <summary> + /// Looks up a localized string similar to `-ModulePath` value must be `CurrentUser`, `AllUsers`, `Default` or an absolute path.. + /// </summary> + internal static string ConfigurationModulePathArgError { + get { + return ResourceManager.GetString("ConfigurationModulePathArgError", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Module: {0} by {1} [{2}]. /// </summary> internal static string ConfigurationModuleWithDetails { diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx @@ -298,4 +298,12 @@ <data name="DebugNotSupported" xml:space="preserve"> <value>Debug parameter not supported</value> </data> + <data name="ConfigurationAllUsersElevated" xml:space="preserve"> + <value>`-ModulePath AllUsers` require administrator privileges to execute.</value> + <comment>{Locked="-ModulePath AllUsers"}</comment> + </data> + <data name="ConfigurationModulePathArgError" xml:space="preserve"> + <value>`-ModulePath` value must be `CurrentUser`, `AllUsers`, `Default` or an absolute path.</value> + <comment>{Locked="{-ModulePath}, {CurrentUser}, {AllUsers}, {Default}}</comment> + </data> </root> \ No newline at end of file