diff --git a/build.psm1 b/build.psm1 index c6eb8624a41..b4548a66725 100644 --- a/build.psm1 +++ b/build.psm1 @@ -587,8 +587,10 @@ Fix steps: -not ($Runtime -like 'fxdependent*')) { $json = & $publishPath\pwsh -noprofile -command { - $expFeatures = [System.Collections.Generic.List[string]]::new() - Get-ExperimentalFeature | ForEach-Object { $expFeatures.Add($_.Name) } + # Special case for DSC code in PS; + # this experimental feature requires new DSC module that is not inbox, + # so we don't want default DSC use case be broken + $expFeatures = Get-ExperimentalFeature | Where-Object Name -NE PS7DscSupport | ForEach-Object -MemberName Name # Make sure ExperimentalFeatures from modules in PSHome are added # https://github.com/PowerShell/PowerShell/issues/10550 @@ -598,7 +600,7 @@ Fix steps: } } - ConvertTo-Json $expFeatures.ToArray() + ConvertTo-Json $expFeatures } $config += @{ ExperimentalFeatures = ([string[]] ($json | ConvertFrom-Json)) } diff --git a/src/System.Management.Automation/DscSupport/CimDSCParser.cs b/src/System.Management.Automation/DscSupport/CimDSCParser.cs index 90a14a3946b..2901148813b 100644 --- a/src/System.Management.Automation/DscSupport/CimDSCParser.cs +++ b/src/System.Management.Automation/DscSupport/CimDSCParser.cs @@ -41,9 +41,7 @@ public static object ConvertCimInstanceToObject(Type targetType, CimInstance ins using (System.Management.Automation.PowerShell powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) { - const string script = "param($targetType,$moduleName) & (Microsoft.PowerShell.Core\\Get-Module $moduleName) { New-Object $targetType } "; - - powerShell.AddScript(script); + powerShell.AddScript("param($targetType,$moduleName) & (Microsoft.PowerShell.Core\\Get-Module $moduleName) { New-Object $targetType } "); powerShell.AddArgument(targetType); powerShell.AddArgument(moduleName); @@ -945,6 +943,20 @@ private static CimClass MyClassCallback(string serverName, string namespaceName, return null; } + /// + /// Reads CIM MOF schema file and returns classes defined in it. + /// This is used MOF->PSClass conversion tool. + /// + /// + /// Path to CIM MOF schema file for reading. + /// + /// List of classes from MOF schema file. + public static List ReadCimSchemaMof(string mofPath) + { + var parser = new Microsoft.PowerShell.DesiredStateConfiguration.CimDSCParser(MyClassCallback); + return parser.ParseSchemaMof(mofPath); + } + /// /// Import CIM classes from the given file. /// diff --git a/src/System.Management.Automation/DscSupport/JsonCimDSCParser.cs b/src/System.Management.Automation/DscSupport/JsonCimDSCParser.cs new file mode 100755 index 00000000000..c2221cb3f33 --- /dev/null +++ b/src/System.Management.Automation/DscSupport/JsonCimDSCParser.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Management.Automation; +using System.Security; + +namespace Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform +{ + /// + /// Class that does high level Cim schema parsing. + /// + internal class CimDSCParser + { + private readonly JsonDeserializer _jsonDeserializer; + + internal CimDSCParser() + { + _jsonDeserializer = JsonDeserializer.Create(); + } + + internal IEnumerable ParseSchemaJson(string filePath, bool useNewRunspace = false) + { + try + { + string json = File.ReadAllText(filePath); + string fileNameDefiningClass = Path.GetFileNameWithoutExtension(filePath); + int dotIndex = fileNameDefiningClass.IndexOf(".schema", StringComparison.InvariantCultureIgnoreCase); + if (dotIndex != -1) + { + fileNameDefiningClass = fileNameDefiningClass.Substring(0, dotIndex); + } + + IEnumerable result = _jsonDeserializer.DeserializeClasses(json, useNewRunspace); + foreach (dynamic classObject in result) + { + string superClassName = classObject.SuperClassName; + string className = classObject.ClassName; + if (string.Equals(superClassName, "OMI_BaseResource", StringComparison.OrdinalIgnoreCase)) + { + // Get the name of the file without schema.mof/json extension + if (!className.Equals(fileNameDefiningClass, StringComparison.OrdinalIgnoreCase)) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException( + ParserStrings.ClassNameNotSameAsDefiningFile, className, fileNameDefiningClass); + throw e; + } + } + } + + return result; + } + catch (Exception exception) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException( + exception, ParserStrings.CimDeserializationError, filePath); + + e.SetErrorId("CimDeserializationError"); + throw e; + } + } + } +} diff --git a/src/System.Management.Automation/DscSupport/JsonDeserializer.cs b/src/System.Management.Automation/DscSupport/JsonDeserializer.cs new file mode 100755 index 00000000000..319560f0a09 --- /dev/null +++ b/src/System.Management.Automation/DscSupport/JsonDeserializer.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform +{ + internal class JsonDeserializer + { + #region Constructors + + /// + /// Instantiates a default deserializer. + /// + /// Default deserializer. + public static JsonDeserializer Create() + { + return new JsonDeserializer(); + } + + #endregion Constructors + + #region Methods + + /// + /// Returns schema of Cim classes from specified json file. + /// + /// Json text to deserialize. + /// If a new runspace should be used. + /// Deserialized PSObjects. + public IEnumerable DeserializeClasses(string json, bool useNewRunspace = false) + { + if (string.IsNullOrEmpty(json)) + { + throw new ArgumentNullException(nameof(json)); + } + + System.Management.Automation.PowerShell powerShell = null; + + if (useNewRunspace) + { + // currently using RunspaceMode.NewRunspace will reset PSModulePath env var for the entire process + // this is something we want to avoid in DSC GuestConfigAgent scenario, so we use following workaround + var s_iss = InitialSessionState.CreateDefault(); + s_iss.EnvironmentVariables.Add( + new SessionStateVariableEntry( + "PSModulePath", + Environment.GetEnvironmentVariable("PSModulePath"), + description: null)); + powerShell = System.Management.Automation.PowerShell.Create(s_iss); + } + else + { + powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + } + + using (powerShell) + { + return powerShell.AddCommand("Microsoft.PowerShell.Utility\\ConvertFrom-Json") + .AddParameter("InputObject", json) + .AddParameter("Depth", 100) // maximum supported by cmdlet + .Invoke(); + } + } + + #endregion Methods + } +} diff --git a/src/System.Management.Automation/DscSupport/JsonDscClassCache.cs b/src/System.Management.Automation/DscSupport/JsonDscClassCache.cs new file mode 100755 index 00000000000..2fe4f7c2606 --- /dev/null +++ b/src/System.Management.Automation/DscSupport/JsonDscClassCache.cs @@ -0,0 +1,2498 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using System.Text.RegularExpressions; + +using Microsoft.PowerShell.Commands; + +namespace Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform +{ + /// + /// Class that defines Dsc cache entries. + /// + internal class DscClassCacheEntry + { + /// + /// Initializes a new instance of the class. + /// + public DscClassCacheEntry() + : this(DSCResourceRunAsCredential.Default, isImportedImplicitly: false, cimClassInstance: null, modulePath: string.Empty) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Run as credential value. + /// Resource is imported implicitly. + /// Class definition. + /// Path of module defining the class. + public DscClassCacheEntry(DSCResourceRunAsCredential dscResourceRunAsCredential, bool isImportedImplicitly, PSObject cimClassInstance, string modulePath) + { + DscResRunAsCred = dscResourceRunAsCredential; + IsImportedImplicitly = isImportedImplicitly; + CimClassInstance = cimClassInstance; + ModulePath = modulePath; + } + + /// + /// Gets or sets the RunAs Credentials that this DSC resource will use. + /// + public DSCResourceRunAsCredential DscResRunAsCred { get; set; } + + /// + /// Gets or sets a value indicating if we have implicitly imported this resource. + /// + public bool IsImportedImplicitly { get; set; } + + /// + /// Gets or sets CimClass instance for this resource. + /// + public PSObject CimClassInstance { get; set; } + + /// + /// Gets or sets path of the implementing module for this resource. + /// + public string ModulePath { get; set; } + } + + /// + /// DSC class cache for this runspace. + /// + [SuppressMessage("Microsoft.MSInternal", "CA903:InternalNamespaceShouldNotContainPublicTypes", + Justification = "Needed Internal use only")] + public static class DscClassCache + { + private static readonly HashSet s_reservedDynamicKeywords = new HashSet(new[] { "Synchronization", "Certificate", "IIS", "SQL" }, StringComparer.OrdinalIgnoreCase); + + private static readonly HashSet s_reservedProperties = new HashSet(new[] { "Require", "Trigger", "Notify", "Before", "After", "Subscribe" }, StringComparer.OrdinalIgnoreCase); + + /// + /// Experimental feature name for DSC v3. + /// + public const string DscExperimentalFeatureName = "PS7DscSupport"; + + private static readonly PSTraceSource s_tracer = PSTraceSource.GetTracer("DSC", "DSC Class Cache"); + + // Constants for items in the module qualified name (Module\Version\ClassName) + private const int ModuleNameIndex = 0; + private const int ModuleVersionIndex = 1; + private const int ClassNameIndex = 2; + private const int FriendlyNameIndex = 3; + + // Create a HashSet for fast lookup. According to MSDN, the time complexity of search for an element in a HashSet is O(1) + private static readonly HashSet s_hiddenResourceCache = + new HashSet(StringComparer.OrdinalIgnoreCase) { "MSFT_BaseConfigurationProviderRegistration", "MSFT_CimConfigurationProviderRegistration", "MSFT_PSConfigurationProviderRegistration" }; + + // A collection to prevent circular importing case when Import-DscResource does not have a module specified + [ThreadStatic] + private static readonly HashSet t_currentImportDscResourceInvocations = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets DSC class cache for this runspace. + /// Cache stores the DSCRunAsBehavior, cim class and boolean to indicate if an Inbox resource has been implicitly imported. + /// + private static Dictionary ClassCache + { + get => t_classCache ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + [ThreadStatic] + private static Dictionary t_classCache; + + /// + /// Gets DSC class cache for GuestConfig; it is similar to ClassCache, but maintains values between operations. + /// + private static Dictionary GuestConfigClassCache + { + get => t_guestConfigClassCache ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + [ThreadStatic] + private static Dictionary t_guestConfigClassCache; + + /// + /// DSC classname to source module mapper. + /// + private static Dictionary> ByClassModuleCache + => t_byClassModuleCache ??= new Dictionary>(StringComparer.OrdinalIgnoreCase); + + [ThreadStatic] + private static Dictionary> t_byClassModuleCache; + + /// + /// Default ModuleName and ModuleVersion to use. + /// + private static readonly Tuple s_defaultModuleInfoForResource = new Tuple("PSDesiredStateConfiguration", new Version(3, 0)); + + /// + /// When this property is set to true, DSC Cache will cache multiple versions of a resource. + /// That means it will cache duplicate resource classes (class names for a resource in two different module versions are same). + /// NOTE: This property should be set to false for DSC compiler related methods/functionality, such as Import-DscResource, + /// because the Mof serializer does not support deserialization of classes with different versions. + /// + [ThreadStatic] + private static bool t_cacheResourcesFromMultipleModuleVersions; + + private static bool CacheResourcesFromMultipleModuleVersions + { + get + { + return t_cacheResourcesFromMultipleModuleVersions; + } + + set + { + t_cacheResourcesFromMultipleModuleVersions = value; + } + } + + [ThreadStatic] + private static bool t_newApiIsUsed = false; + + /// + /// Flag shows if PS7 DSC APIs were used. + /// + public static bool NewApiIsUsed + { + get + { + return t_newApiIsUsed; + } + + set + { + t_newApiIsUsed = value; + } + } + + /// + /// Initialize the class cache with the default classes in $ENV:SystemDirectory\Configuration. + /// + public static void Initialize() + { + Initialize(errors: null, modulePathList: null); + } + + /// + /// Initialize the class cache with default classes that come with PSDesiredStateConfiguration module. + /// + /// Collection of any errors encountered during initialization. + /// List of module path from where DSC PS modules will be loaded. + public static void Initialize(Collection errors, List modulePathList) + { + s_tracer.WriteLine("Initializing DSC class cache"); + + // Load the base schema files. + ClearCache(); + var dscConfigurationDirectory = Environment.GetEnvironmentVariable("DSC_HOME"); + if (string.IsNullOrEmpty(dscConfigurationDirectory)) + { + var moduleInfos = ModuleCmdletBase.GetModuleIfAvailable(new Microsoft.PowerShell.Commands.ModuleSpecification() + { + Name = "PSDesiredStateConfiguration", + + // Version in the next line is actually MinimumVersion + Version = new Version(3, 0, 0) + }); + + if (moduleInfos.Count > 0) + { + // to be consistent with Import-Module behavior, we use the first occurrence that we find in PSModulePath + var moduleDirectory = Path.GetDirectoryName(moduleInfos[0].Path); + dscConfigurationDirectory = Path.Join(moduleDirectory, "Configuration"); + } + else + { + // when all else has failed use location of system-wide PS module directory (i.e. /usr/local/share/powershell/Modules) as backup + dscConfigurationDirectory = Path.Join(ModuleIntrinsics.GetSharedModulePath(), "PSDesiredStateConfiguration", "Configuration"); + } + } + + if (!Directory.Exists(dscConfigurationDirectory)) + { + throw new DirectoryNotFoundException(string.Format(ParserStrings.PsDscMissingSchemaStore, dscConfigurationDirectory)); + } + + var resourceBaseFile = Path.Join(dscConfigurationDirectory, "BaseRegistration", "BaseResource.schema.json"); + ImportBaseClasses(resourceBaseFile, s_defaultModuleInfoForResource, errors, false); + var metaConfigFile = Path.Join(dscConfigurationDirectory, "BaseRegistration", "MSFT_DSCMetaConfiguration.json"); + ImportBaseClasses(metaConfigFile, s_defaultModuleInfoForResource, errors, false); + } + + /// + /// Import base classes from the given file. + /// + /// Path to schema file. + /// Module information. + /// Error collection that will be shown to the user. + /// Flag for implicitly imported resource. + /// Class objects from schema file. + public static IEnumerable ImportBaseClasses(string path, Tuple moduleInfo, Collection errors, bool importInBoxResourcesImplicitly) + { + if (string.IsNullOrEmpty(path)) + { + throw PSTraceSource.NewArgumentNullException(nameof(path)); + } + + s_tracer.WriteLine("DSC ClassCache: importing file: {0}", path); + + var parser = new CimDSCParser(); + + IEnumerable classes = null; + try + { + classes = parser.ParseSchemaJson(path); + } + catch (PSInvalidOperationException e) + { + // Ignore modules with invalid schemas. + s_tracer.WriteLine("DSC ClassCache: Error importing file '{0}', with error '{1}'. Skipping file.", path, e); + if (errors != null) + { + errors.Add(e); + } + } + + if (classes != null) + { + foreach (dynamic c in classes) + { + var className = c.ClassName; + + if (string.IsNullOrEmpty(className)) + { + // ClassName is empty - skipping class import + continue; + } + + string alias = GetFriendlyName(c); + var friendlyName = string.IsNullOrEmpty(alias) ? className : alias; + string moduleQualifiedResourceName = GetModuleQualifiedResourceName(moduleInfo.Item1, moduleInfo.Item2.ToString(), className, friendlyName); + DscClassCacheEntry cimClassInfo; + + if (ClassCache.TryGetValue(moduleQualifiedResourceName, out cimClassInfo)) + { + if (errors != null) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException( + ParserStrings.DuplicateCimClassDefinition, className, path, cimClassInfo.ModulePath); + + e.SetErrorId("DuplicateCimClassDefinition"); + errors.Add(e); + } + + continue; + } + + if (s_hiddenResourceCache.Contains(className)) + { + continue; + } + + var classCacheEntry = new DscClassCacheEntry(DSCResourceRunAsCredential.NotSupported, importInBoxResourcesImplicitly, c, path); + ClassCache[moduleQualifiedResourceName] = classCacheEntry; + GuestConfigClassCache[moduleQualifiedResourceName] = classCacheEntry; + ByClassModuleCache[className] = moduleInfo; + } + + var sb = new System.Text.StringBuilder(); + foreach (dynamic c in classes) + { + sb.Append(c.ClassName); + sb.Append(','); + } + + s_tracer.WriteLine("DSC ClassCache: loading file '{0}' added the following classes to the cache: {1}", path, sb.ToString()); + } + else + { + s_tracer.WriteLine("DSC ClassCache: loading file '{0}' added no classes to the cache."); + } + + return classes; + } + + /// + /// Get text from SecureString. + /// + /// Value of SecureString. + /// Decoded string. + public static string GetStringFromSecureString(SecureString value) + { + string passwordValueToAdd = string.Empty; + + if (value != null) + { + IntPtr ptr = Marshal.SecureStringToCoTaskMemUnicode(value); + passwordValueToAdd = Marshal.PtrToStringUni(ptr); + Marshal.ZeroFreeCoTaskMemUnicode(ptr); + } + + return passwordValueToAdd; + } + + /// + /// Clear out the existing collection of CIM classes and associated keywords. + /// + public static void ClearCache() + { + if (!ExperimentalFeature.IsEnabled(DscExperimentalFeatureName)) + { + throw new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); + } + + s_tracer.WriteLine("DSC class: clearing the cache and associated keywords."); + ClassCache.Clear(); + ByClassModuleCache.Clear(); + CacheResourcesFromMultipleModuleVersions = false; + t_currentImportDscResourceInvocations.Clear(); + } + + private static string GetModuleQualifiedResourceName(string moduleName, string moduleVersion, string className, string resourceName) + { + return string.Format(CultureInfo.InvariantCulture, "{0}\\{1}\\{2}\\{3}", moduleName, moduleVersion, className, resourceName); + } + + private static List> FindResourceInCache(string moduleName, string className, string resourceName) + { + return (from cacheEntry in ClassCache + let splittedName = cacheEntry.Key.Split(Utils.Separators.Backslash) + let cachedClassName = splittedName[ClassNameIndex] + let cachedModuleName = splittedName[ModuleNameIndex] + let cachedResourceName = splittedName[FriendlyNameIndex] + where (string.Equals(cachedResourceName, resourceName, StringComparison.OrdinalIgnoreCase) + || (string.Equals(cachedClassName, className, StringComparison.OrdinalIgnoreCase) + && string.Equals(cachedModuleName, moduleName, StringComparison.OrdinalIgnoreCase))) + select cacheEntry).ToList(); + } + + /// + /// Returns class declaration from GuestConfigClassCache. + /// + /// Module name. + /// Module version. + /// Name of the class. + /// Friendly name of the resource. + /// Class declaration from cache. + public static PSObject GetGuestConfigCachedClass(string moduleName, string moduleVersion, string className, string resourceName) + { + if (!ExperimentalFeature.IsEnabled(DscExperimentalFeatureName)) + { + throw new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); + } + + var moduleQualifiedResourceName = GetModuleQualifiedResourceName(moduleName, moduleVersion, className, string.IsNullOrEmpty(resourceName) ? className : resourceName); + DscClassCacheEntry classCacheEntry = null; + if (GuestConfigClassCache.TryGetValue(moduleQualifiedResourceName, out classCacheEntry)) + { + return classCacheEntry.CimClassInstance; + } + else + { + // if class was not found with current ResourceName then it may be a class with non-empty FriendlyName that caller does not know, so perform a broad search + string partialClassPath = string.Join('\\', moduleName, moduleVersion, className, string.Empty); + foreach (string key in GuestConfigClassCache.Keys) + { + if (key.StartsWith(partialClassPath)) + { + return GuestConfigClassCache[key].CimClassInstance; + } + } + + return null; + } + } + + /// + /// Clears GuestConfigClassCache. + /// + public static void ClearGuestConfigClassCache() + { + GuestConfigClassCache.Clear(); + } + + private static bool IsMagicProperty(string propertyName) + { + return System.Text.RegularExpressions.Regex.Match(propertyName, "^(ResourceId|SourceInfo|ModuleName|ModuleVersion|ConfigurationName)$", System.Text.RegularExpressions.RegexOptions.IgnoreCase).Success; + } + + private static string GetFriendlyName(dynamic cimClass) + { + return cimClass.FriendlyName; + } + + /// + /// Method to get the cached classes in the form of DynamicKeyword. + /// + /// Dynamic keyword collection. + public static Collection GetKeywordsFromCachedClasses() + { + if (!ExperimentalFeature.IsEnabled(DscExperimentalFeatureName)) + { + throw new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); + } + + Collection keywords = new Collection(); + + foreach (KeyValuePair cachedClass in ClassCache) + { + string[] splittedName = cachedClass.Key.Split(Utils.Separators.Backslash); + string moduleName = splittedName[ModuleNameIndex]; + string moduleVersion = splittedName[ModuleVersionIndex]; + + var keyword = CreateKeywordFromCimClass(moduleName, Version.Parse(moduleVersion), cachedClass.Value.CimClassInstance, cachedClass.Value.DscResRunAsCred); + if (keyword is not null) + { + keywords.Add(keyword); + } + } + + return keywords; + } + + private static void CreateAndRegisterKeywordFromCimClass(string moduleName, Version moduleVersion, PSObject cimClass, Dictionary functionsToDefine, DSCResourceRunAsCredential runAsBehavior) + { + var keyword = CreateKeywordFromCimClass(moduleName, moduleVersion, cimClass, runAsBehavior); + if (keyword is null) + { + return; + } + + // keyword is already defined and we don't allow redefine it + if (!CacheResourcesFromMultipleModuleVersions && DynamicKeyword.ContainsKeyword(keyword.Keyword)) + { + var oldKeyword = DynamicKeyword.GetKeyword(keyword.Keyword); + if (oldKeyword.ImplementingModule is null || + !oldKeyword.ImplementingModule.Equals(moduleName, StringComparison.OrdinalIgnoreCase) || oldKeyword.ImplementingModuleVersion != moduleVersion) + { + var e = PSTraceSource.NewInvalidOperationException(ParserStrings.DuplicateKeywordDefinition, keyword.Keyword); + e.SetErrorId("DuplicateKeywordDefinition"); + throw e; + } + } + + // Add the dynamic keyword to the table + DynamicKeyword.AddKeyword(keyword); + + // And now define the driver functions in the current scope... + if (functionsToDefine != null) + { + functionsToDefine[moduleName + "\\" + keyword.Keyword] = CimKeywordImplementationFunction; + } + } + + private static DynamicKeyword CreateKeywordFromCimClass(string moduleName, Version moduleVersion, dynamic cimClass, DSCResourceRunAsCredential runAsBehavior) + { + var resourceName = cimClass.ClassName; + string alias = GetFriendlyName(cimClass); + var keywordString = string.IsNullOrEmpty(alias) ? resourceName : alias; + + // Skip all of the base, meta, registration and other classes that are not intended to be used directly by a script author + if (System.Text.RegularExpressions.Regex.Match(keywordString, "^OMI_Base|^OMI_.*Registration", System.Text.RegularExpressions.RegexOptions.IgnoreCase).Success) + { + return null; + } + + var keyword = new DynamicKeyword() + { + BodyMode = DynamicKeywordBodyMode.Hashtable, + Keyword = keywordString, + ResourceName = resourceName, + ImplementingModule = moduleName, + ImplementingModuleVersion = moduleVersion, + SemanticCheck = CheckMandatoryPropertiesPresent + }; + + // If it's one of reserved dynamic keyword, mark it + if (s_reservedDynamicKeywords.Contains(keywordString)) + { + keyword.IsReservedKeyword = true; + } + + // see if it's a resource type i.e. it inherits from OMI_BaseResource + bool isResourceType = false; + + // previous version of this code was the only place that referenced CimSuperClass + // so to simplify things we just check superclass to be OMI_BaseResource + // with assumption that current code will not work for multi-level class inheritance (which is never used in practice according to DSC team) + // this simplification allows us to avoid linking objects together using CimSuperClass field during deserialization + if ((!string.IsNullOrEmpty(cimClass.SuperClassName)) && string.Equals("OMI_BaseResource", cimClass.SuperClassName, StringComparison.OrdinalIgnoreCase)) + { + isResourceType = true; + } + + // If it's a resource type, then a resource name is required. + keyword.NameMode = isResourceType ? DynamicKeywordNameMode.NameRequired : DynamicKeywordNameMode.NoName; + + // Add the settable properties to the keyword object + if (cimClass.ClassProperties != null) + { + foreach (var prop in cimClass.ClassProperties) + { + // If the property has the Read qualifier, skip it. + if (string.Equals(prop.Qualifiers?.Read?.ToString(), "True", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // If it's one of our magic properties, skip it + if (IsMagicProperty(prop.Name)) + { + continue; + } + + if (runAsBehavior == DSCResourceRunAsCredential.NotSupported) + { + if (string.Equals(prop.Name, "PsDscRunAsCredential", StringComparison.OrdinalIgnoreCase)) + { + // skip adding PsDscRunAsCredential to the dynamic word for the dsc resource. + continue; + } + } + + // If it's one of our reserved properties, save it for error reporting + if (s_reservedProperties.Contains(prop.Name)) + { + keyword.HasReservedProperties = true; + continue; + } + + // Otherwise, add it to the Keyword List. + var keyProp = new System.Management.Automation.Language.DynamicKeywordProperty(); + keyProp.Name = prop.Name; + + // Copy the type name string. If it's an embedded instance, need to grab it from the ReferenceClassName + bool referenceClassNameIsNullOrEmpty = string.IsNullOrEmpty(prop.ReferenceClassName); + if (prop.CimType == "Instance" && !referenceClassNameIsNullOrEmpty) + { + keyProp.TypeConstraint = prop.ReferenceClassName; + } + else if (prop.CimType == "InstanceArray" && !referenceClassNameIsNullOrEmpty) + { + keyProp.TypeConstraint = prop.ReferenceClassName + "[]"; + } + else + { + keyProp.TypeConstraint = prop.CimType.ToString(); + } + + // Check to see if there is a Values attribute and save the list of allowed values if so. + var values = prop.Qualifiers?.Values; + if (values is not null) + { + foreach (var val in values) + { + keyProp.Values.Add(val.ToString()); + } + } + + // Check to see if there is a ValueMap attribute and save the list of allowed values if so. + var nativeValueMap = prop.Qualifiers?.ValueMap; + List valueMap = null; + if (nativeValueMap is not null) + { + valueMap = new List(); + foreach (var val in nativeValueMap) + { + valueMap.Add(val.ToString()); + } + } + + // Check to see if this property has the Required qualifier associated with it. + if (string.Equals(prop.Qualifiers?.Required?.ToString(), "True", StringComparison.OrdinalIgnoreCase)) + { + keyProp.Mandatory = true; + } + + // Check to see if this property has the Key qualifier associated with it. + if (string.Equals(prop.Qualifiers?.Key?.ToString(), "True", StringComparison.OrdinalIgnoreCase)) + { + keyProp.Mandatory = true; + keyProp.IsKey = true; + } + + // set the property to mandatory is specified for the resource. + if (runAsBehavior == DSCResourceRunAsCredential.Mandatory) + { + if (string.Equals(prop.Name, "PsDscRunAsCredential", StringComparison.OrdinalIgnoreCase)) + { + keyProp.Mandatory = true; + } + } + + if (valueMap is not null && keyProp.Values.Count > 0) + { + if (valueMap.Count != keyProp.Values.Count) + { + s_tracer.WriteLine( + "DSC CreateDynamicKeywordFromClass: the count of values for qualifier 'Values' and 'ValueMap' doesn't match. count of 'Values': {0}, count of 'ValueMap': {1}. Skip the keyword '{2}'.", + keyProp.Values.Count, + valueMap.Count, + keyword.Keyword); + return null; + } + + for (int index = 0; index < valueMap.Count; index++) + { + string key = keyProp.Values[index]; + string value = valueMap[index]; + + if (keyProp.ValueMap.ContainsKey(key)) + { + s_tracer.WriteLine( + "DSC CreateDynamicKeywordFromClass: same string value '{0}' appears more than once in qualifier 'Values'. Skip the keyword '{1}'.", + key, + keyword.Keyword); + return null; + } + + keyProp.ValueMap.Add(key, value); + } + } + + keyword.Properties.Add(prop.Name, keyProp); + } + } + + // update specific keyword with range constraints + UpdateKnownRestriction(keyword); + + return keyword; + } + + private static void UpdateKnownRestriction(DynamicKeyword keyword) + { + const int RefreshFrequencyMin = 30; + const int RefreshFrequencyMax = 44640; + + const int ConfigurationModeFrequencyMin = 15; + const int ConfigurationModeFrequencyMax = 44640; + + if ( + string.Equals( + keyword.ResourceName, + "MSFT_DSCMetaConfigurationV2", + StringComparison.OrdinalIgnoreCase) + || + string.Equals( + keyword.ResourceName, + "MSFT_DSCMetaConfiguration", + StringComparison.OrdinalIgnoreCase)) + { + if (keyword.Properties["RefreshFrequencyMins"] is not null) + { + keyword.Properties["RefreshFrequencyMins"].Range = new Tuple(RefreshFrequencyMin, RefreshFrequencyMax); + } + + if (keyword.Properties["ConfigurationModeFrequencyMins"] != null) + { + keyword.Properties["ConfigurationModeFrequencyMins"].Range = new Tuple(ConfigurationModeFrequencyMin, ConfigurationModeFrequencyMax); + } + + if (keyword.Properties["DebugMode"] is not null) + { + keyword.Properties["DebugMode"].Values.Remove("ResourceScriptBreakAll"); + keyword.Properties["DebugMode"].ValueMap.Remove("ResourceScriptBreakAll"); + } + } + } + + /// + /// Load the default system CIM classes and create the corresponding keywords. + /// + /// Collection of any errors encountered while loading keywords. + public static void LoadDefaultCimKeywords(Collection errors) + { + LoadDefaultCimKeywords(functionsToDefine: null, errors, modulePathList: null, cacheResourcesFromMultipleModuleVersions: false); + } + + /// + /// Load the default system CIM classes and create the corresponding keywords. + /// + /// A dictionary to add the defined functions to, may be null. + public static void LoadDefaultCimKeywords(Dictionary functionsToDefine) + { + LoadDefaultCimKeywords(functionsToDefine, errors: null, modulePathList: null, cacheResourcesFromMultipleModuleVersions: false); + } + + /// + /// Load the default system CIM classes and create the corresponding keywords. + /// + /// Collection of any errors encountered while loading keywords. + /// Allow caching the resources from multiple versions of modules. + public static void LoadDefaultCimKeywords(Collection errors, bool cacheResourcesFromMultipleModuleVersions) + { + LoadDefaultCimKeywords(functionsToDefine: null, errors, modulePathList: null, cacheResourcesFromMultipleModuleVersions); + } + + /// + /// Load the default system CIM classes and create the corresponding keywords. + /// + /// A dictionary to add the defined functions to, may be null. + /// Collection of any errors encountered while loading keywords. + /// List of module path from where DSC PS modules will be loaded. + /// Allow caching the resources from multiple versions of modules. + private static void LoadDefaultCimKeywords( + Dictionary functionsToDefine, + Collection errors, + List modulePathList, + bool cacheResourcesFromMultipleModuleVersions) + { + if (!ExperimentalFeature.IsEnabled(DscExperimentalFeatureName)) + { + Exception exception = new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); + errors.Add(exception); + return; + } + + NewApiIsUsed = true; + DynamicKeyword.Reset(); + Initialize(errors, modulePathList); + + // Initialize->ClearCache resets CacheResourcesFromMultipleModuleVersions to false, + // workaround is to set it after Initialize method call. + // Initialize method imports all the Inbox resources and internal classes which belongs to only one version + // of the module, so it is ok if this property is not set during cache initialization. + CacheResourcesFromMultipleModuleVersions = cacheResourcesFromMultipleModuleVersions; + + foreach (dynamic cimClass in ClassCache.Values) + { + var className = cimClass.CimClassInstance.ClassName; + var moduleInfo = ByClassModuleCache[className]; + CreateAndRegisterKeywordFromCimClass(moduleInfo.Item1, moduleInfo.Item2, cimClass.CimClassInstance, functionsToDefine, cimClass.DscResRunAsCred); + } + + // And add the Node keyword definitions + if (!DynamicKeyword.ContainsKeyword("Node")) + { + // Implement dispatch to the Node keyword. + var nodeKeyword = new DynamicKeyword() + { + BodyMode = DynamicKeywordBodyMode.ScriptBlock, + ImplementingModule = s_defaultModuleInfoForResource.Item1, + ImplementingModuleVersion = s_defaultModuleInfoForResource.Item2, + NameMode = DynamicKeywordNameMode.NameRequired, + Keyword = "Node", + }; + DynamicKeyword.AddKeyword(nodeKeyword); + } + + // And add the Import-DscResource keyword definitions + if (!DynamicKeyword.ContainsKeyword("Import-DscResource")) + { + // Implement dispatch to the Node keyword. + var nodeKeyword = new DynamicKeyword() + { + BodyMode = DynamicKeywordBodyMode.Command, + ImplementingModule = s_defaultModuleInfoForResource.Item1, + ImplementingModuleVersion = s_defaultModuleInfoForResource.Item2, + NameMode = DynamicKeywordNameMode.NoName, + Keyword = "Import-DscResource", + MetaStatement = true, + PostParse = ImportResourcePostParse, + SemanticCheck = ImportResourceCheckSemantics + }; + DynamicKeyword.AddKeyword(nodeKeyword); + } + } + + // This function is called after parsing the Import-DscResource keyword and it's arguments, but before parsing anything else. + private static ParseError[] ImportResourcePostParse(DynamicKeywordStatementAst ast) + { + var elements = Ast.CopyElements(ast.CommandElements); + var commandAst = new CommandAst(ast.Extent, elements, TokenKind.Unknown, null); + + const string NameParam = "Name"; + const string ModuleNameParam = "ModuleName"; + const string ModuleVersionParam = "ModuleVersion"; + + StaticBindingResult bindingResult = StaticParameterBinder.BindCommand(commandAst, false); + + var errorList = new List(); + foreach (var bindingException in bindingResult.BindingExceptions.Values) + { + errorList.Add(new ParseError(bindingException.CommandElement.Extent, "ParameterBindingException", bindingException.BindingException.Message)); + } + + ParameterBindingResult moduleNameBindingResult = null; + ParameterBindingResult resourceNameBindingResult = null; + ParameterBindingResult moduleVersionBindingResult = null; + + foreach (var binding in bindingResult.BoundParameters) + { + // Error case when positional parameter values are specified + var boundParameterName = binding.Key; + var parameterBindingResult = binding.Value; + if (boundParameterName.All(char.IsDigit)) + { + errorList.Add(new ParseError(parameterBindingResult.Value.Extent, "ImportDscResourcePositionalParamsNotSupported", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourcePositionalParamsNotSupported))); + continue; + } + + if (NameParam.StartsWith(boundParameterName, StringComparison.OrdinalIgnoreCase)) + { + resourceNameBindingResult = parameterBindingResult; + } + else if (ModuleNameParam.StartsWith(boundParameterName, StringComparison.OrdinalIgnoreCase)) + { + moduleNameBindingResult = parameterBindingResult; + } + else if (ModuleVersionParam.StartsWith(boundParameterName, StringComparison.OrdinalIgnoreCase)) + { + moduleVersionBindingResult = parameterBindingResult; + } + else + { + errorList.Add(new ParseError(parameterBindingResult.Value.Extent, "ImportDscResourceNeedParams", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); + } + } + + if (errorList.Count == 0 && moduleNameBindingResult == null && resourceNameBindingResult == null) + { + errorList.Add(new ParseError(ast.Extent, "ImportDscResourceNeedParams", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); + } + + // Check here if Version is specified but modulename is not specified + if (moduleVersionBindingResult != null && moduleNameBindingResult == null) + { + // only add this error again to the error list if resources is not null + // if resources and modules are both null we have already added this error in collection + // we do not want to do this twice. since we are giving same error ImportDscResourceNeedParams in both cases + // once we have different error messages for 2 scenarios we can remove this check + if (resourceNameBindingResult is not null) + { + errorList.Add(new ParseError(ast.Extent, "ImportDscResourceNeedModuleNameWithModuleVersion", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); + } + } + + string[] resourceNames = null; + if (resourceNameBindingResult is not null) + { + object resourceName = null; + if (!IsConstantValueVisitor.IsConstant(resourceNameBindingResult.Value, out resourceName, true, true) || + !LanguagePrimitives.TryConvertTo(resourceName, out resourceNames)) + { + errorList.Add(new ParseError(resourceNameBindingResult.Value.Extent, "RequiresInvalidStringArgument", string.Format(CultureInfo.CurrentCulture, ParserStrings.RequiresInvalidStringArgument, NameParam))); + } + } + + System.Version moduleVersion = null; + if (moduleVersionBindingResult is not null) + { + object moduleVer = null; + if (!IsConstantValueVisitor.IsConstant(moduleVersionBindingResult.Value, out moduleVer, true, true)) + { + errorList.Add(new ParseError(moduleVersionBindingResult.Value.Extent, "RequiresArgumentMustBeConstant", ParserStrings.RequiresArgumentMustBeConstant)); + } + + if (moduleVer is double) + { + // this happens in case -ModuleVersion 1.0, then use extent text for that. + // The better way to do it would be define static binding API against CommandInfo, that holds information about parameter types. + // This way, we can avoid this ugly special-casing and say that -ModuleVersion has type [System.Version]. + moduleVer = moduleVersionBindingResult.Value.Extent.Text; + } + + if (!LanguagePrimitives.TryConvertTo(moduleVer, out moduleVersion)) + { + errorList.Add(new ParseError(moduleVersionBindingResult.Value.Extent, "RequiresVersionInvalid", ParserStrings.RequiresVersionInvalid)); + } + } + + ModuleSpecification[] moduleSpecifications = null; + if (moduleNameBindingResult is not null) + { + object moduleName = null; + if (!IsConstantValueVisitor.IsConstant(moduleNameBindingResult.Value, out moduleName, true, true)) + { + errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "RequiresArgumentMustBeConstant", ParserStrings.RequiresArgumentMustBeConstant)); + } + + if (LanguagePrimitives.TryConvertTo(moduleName, out moduleSpecifications)) + { + // if resourceNames are specified then we can not specify multiple modules name + if (moduleSpecifications is not null && moduleSpecifications.Length > 1 && resourceNames is not null) + { + errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "ImportDscResourceMultipleModulesNotSupportedWithName", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceMultipleModulesNotSupportedWithName))); + } + + // if moduleversion is specified then we can not specify multiple modules name + if (moduleSpecifications is not null && moduleSpecifications.Length > 1 && moduleVersion is not null) + { + errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "ImportDscResourceMultipleModulesNotSupportedWithVersion", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); + } + + // if moduleversion is specified then we can not specify another version in modulespecification object of ModuleName + if (moduleSpecifications is not null && (moduleSpecifications[0].Version is not null || moduleSpecifications[0].MaximumVersion is not null) && moduleVersion is not null) + { + errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "ImportDscResourceMultipleModuleVersionsNotSupported", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); + } + + // If moduleVersion is specified we have only one module Name in valid scenario + // So update it's version property in module specification object that will be used to load modules + if (moduleSpecifications is not null && moduleSpecifications[0].Version is null && moduleSpecifications[0].MaximumVersion is null && moduleVersion is not null) + { + moduleSpecifications[0].Version = moduleVersion; + } + } + else + { + errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "RequiresInvalidStringArgument", string.Format(CultureInfo.CurrentCulture, ParserStrings.RequiresInvalidStringArgument, ModuleNameParam))); + } + } + + if (errorList.Count == 0) + { + // No errors, try to load the resources + LoadResourcesFromModuleInImportResourcePostParse(ast.Extent, moduleSpecifications, resourceNames, errorList); + } + + return errorList.ToArray(); + } + + // This function performs semantic checks for Import-DscResource + private static ParseError[] ImportResourceCheckSemantics(DynamicKeywordStatementAst ast) + { + List errorList = null; + + var keywordAst = Ast.GetAncestorAst(ast.Parent); + while (keywordAst is not null) + { + if (keywordAst.Keyword.Keyword.Equals("Node")) + { + if (errorList is null) + { + errorList = new List(); + } + + errorList.Add(new ParseError(ast.Extent, "ImportDscResourceInsideNode", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceInsideNode))); + break; + } + + keywordAst = Ast.GetAncestorAst(keywordAst.Parent); + } + + if (errorList is not null) + { + return errorList.ToArray(); + } + else + { + return null; + } + } + + // This function performs semantic checks for all DSC Resources keywords. + private static ParseError[] CheckMandatoryPropertiesPresent(DynamicKeywordStatementAst ast) + { + HashSet mandatoryPropertiesNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var pair in ast.Keyword.Properties) + { + if (pair.Value.Mandatory) + { + mandatoryPropertiesNames.Add(pair.Key); + } + } + + // by design mandatoryPropertiesNames are not empty at this point: + // every resource must have at least one Key property. + HashtableAst hashtableAst = null; + foreach (var commandElementsAst in ast.CommandElements) + { + hashtableAst = commandElementsAst as HashtableAst; + if (hashtableAst != null) + { + break; + } + } + + if (hashtableAst is null) + { + // nothing to validate + return null; + } + + foreach (var pair in hashtableAst.KeyValuePairs) + { + object evalResultObject; + if (IsConstantValueVisitor.IsConstant(pair.Item1, out evalResultObject, forAttribute: false, forRequires: false)) + { + var presentName = evalResultObject as string; + if (presentName is not null) + { + if (mandatoryPropertiesNames.Remove(presentName) && mandatoryPropertiesNames.Count == 0) + { + // optimization, once all mandatory properties are specified, we can safely exit. + return null; + } + } + } + } + + if (mandatoryPropertiesNames.Count > 0) + { + ParseError[] errors = new ParseError[mandatoryPropertiesNames.Count]; + var extent = ast.CommandElements[0].Extent; + int i = 0; + foreach (string name in mandatoryPropertiesNames) + { + errors[i] = new ParseError( + extent, + "MissingValueForMandatoryProperty", + string.Format( + CultureInfo.CurrentCulture, + ParserStrings.MissingValueForMandatoryProperty, + ast.Keyword.Keyword, + ast.Keyword.Properties.First(p => StringComparer.OrdinalIgnoreCase.Equals(p.Value.Name, name)).Value.TypeConstraint, + name)); + i++; + } + + return errors; + } + + return null; + } + + /// + /// Load DSC resources from specified module. + /// + /// Script statement loading the module, can be null. + /// Module information, can be null. + /// Name of the resource to be loaded from module. + /// List of errors reported by the method. + internal static void LoadResourcesFromModuleInImportResourcePostParse( + IScriptExtent scriptExtent, + ModuleSpecification[] moduleSpecifications, + string[] resourceNames, + List errorList) + { + // get all required modules + var modules = new Collection(); + if (moduleSpecifications is not null) + { + foreach (var moduleToImport in moduleSpecifications) + { + bool foundModule = false; + var moduleInfos = ModuleCmdletBase.GetModuleIfAvailable(moduleToImport); + + if (moduleInfos.Count >= 1 && (moduleToImport.Version is not null || moduleToImport.Guid is not null)) + { + foreach (var psModuleInfo in moduleInfos) + { + if ((moduleToImport.Guid.HasValue && moduleToImport.Guid.Equals(psModuleInfo.Guid)) || + (moduleToImport.Version is not null && + moduleToImport.Version.Equals(psModuleInfo.Version))) + { + modules.Add(psModuleInfo); + foundModule = true; + break; + } + } + } + else if (moduleInfos.Count == 1) + { + modules.Add(moduleInfos[0]); + foundModule = true; + } + + if (!foundModule) + { + if (moduleInfos.Count > 1) + { + errorList.Add( + new ParseError( + scriptExtent, + "MultipleModuleEntriesFoundDuringParse", + string.Format(CultureInfo.CurrentCulture, ParserStrings.MultipleModuleEntriesFoundDuringParse, moduleToImport.Name))); + } + else + { + string moduleString = moduleToImport.Version == null + ? moduleToImport.Name + : string.Format(CultureInfo.CurrentCulture, "<{0}, {1}>", moduleToImport.Name, moduleToImport.Version); + + errorList.Add(new ParseError(scriptExtent, "ModuleNotFoundDuringParse", string.Format(CultureInfo.CurrentCulture, ParserStrings.ModuleNotFoundDuringParse, moduleString))); + } + + return; + } + } + } + else if (resourceNames is not null) + { + // Lookup the required resources under available PowerShell modules when modulename is not specified + // Make sure that this is not a circular import/parsing + var callLocation = string.Join(':', scriptExtent.File, scriptExtent.StartLineNumber, scriptExtent.StartColumnNumber, scriptExtent.Text); + if (!t_currentImportDscResourceInvocations.Contains(callLocation)) + { + t_currentImportDscResourceInvocations.Add(callLocation); + using (var powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + powerShell.AddCommand("Get-Module"); + powerShell.AddParameter("ListAvailable"); + modules = powerShell.Invoke(); + } + } + } + + // When ModuleName only specified, we need to import all resources from that module + var resourcesToImport = new List(); + if (resourceNames is null || resourceNames.Length == 0) + { + resourcesToImport.Add("*"); + } + else + { + resourcesToImport.AddRange(resourceNames); + } + + foreach (var moduleInfo in modules) + { + var resourcesFound = new List(); + var exceptionList = new System.Collections.ObjectModel.Collection(); + LoadPowerShellClassResourcesFromModule(primaryModuleInfo: moduleInfo, moduleInfo: moduleInfo, resourcesToImport: resourcesToImport, resourcesFound: resourcesFound, errorList: exceptionList, functionsToDefine: null, recurse: true, extent: scriptExtent); + foreach (Exception ex in exceptionList) + { + errorList.Add(new ParseError(scriptExtent, "ClassResourcesLoadingFailed", ex.Message)); + } + + foreach (var resource in resourcesFound) + { + resourcesToImport.Remove(resource); + } + + if (resourcesToImport.Count == 0) + { + break; + } + } + + if (resourcesToImport.Count > 0) + { + foreach (var resourceNameToImport in resourcesToImport) + { + if (!resourceNameToImport.Contains('*')) + { + errorList.Add(new ParseError(scriptExtent, "DscResourcesNotFoundDuringParsing", string.Format(CultureInfo.CurrentCulture, ParserStrings.DscResourcesNotFoundDuringParsing, resourceNameToImport))); + } + } + } + } + + private static void LoadPowerShellClassResourcesFromModule( + PSModuleInfo primaryModuleInfo, + PSModuleInfo moduleInfo, + ICollection resourcesToImport, + ICollection resourcesFound, + Collection errorList, + Dictionary functionsToDefine = null, + bool recurse = true, + IScriptExtent extent = null) + { + if (primaryModuleInfo._declaredDscResourceExports is null || primaryModuleInfo._declaredDscResourceExports.Count == 0) + { + return; + } + + if (moduleInfo.ModuleType == ModuleType.Binary) + { + throw PSTraceSource.NewArgumentException("isConfiguration", ParserStrings.ConfigurationNotSupportedInPowerShellCore); + } + else + { + string scriptPath = null; + if (moduleInfo.RootModule is not null) + { + scriptPath = Path.Join(moduleInfo.ModuleBase, moduleInfo.RootModule); + } + else if (moduleInfo.Path is not null) + { + scriptPath = moduleInfo.Path; + } + + LoadPowerShellClassResourcesFromModule(scriptPath, primaryModuleInfo, resourcesToImport, resourcesFound, functionsToDefine, errorList, extent); + } + + if (moduleInfo.NestedModules is not null && recurse) + { + foreach (var nestedModule in moduleInfo.NestedModules) + { + LoadPowerShellClassResourcesFromModule(primaryModuleInfo, nestedModule, resourcesToImport, resourcesFound, errorList, functionsToDefine, recurse: false, extent: extent); + } + } + } + + /// + /// Import class resources from module. + /// + /// Module information. + /// Collection of resources to import. + /// Functions to define. + /// List of errors to return. + /// The list of resources imported from this module. + public static List ImportClassResourcesFromModule(PSModuleInfo moduleInfo, ICollection resourcesToImport, Dictionary functionsToDefine, Collection errors) + { + if (!ExperimentalFeature.IsEnabled(DscExperimentalFeatureName)) + { + throw new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); + } + + var resourcesImported = new List(); + LoadPowerShellClassResourcesFromModule(moduleInfo, moduleInfo, resourcesToImport, resourcesImported, errors, functionsToDefine); + return resourcesImported; + } + + internal static PSObject[] GenerateJsonClassesForAst(TypeDefinitionAst typeAst, PSModuleInfo module, DSCResourceRunAsCredential runAsBehavior) + { + var embeddedInstanceTypes = new List(); + + var result = GenerateJsonClassesForAst(typeAst, embeddedInstanceTypes); + var visitedInstances = new List(); + visitedInstances.Add(typeAst); + var classes = ProcessEmbeddedInstanceTypes(embeddedInstanceTypes, visitedInstances); + AddEmbeddedInstanceTypesToCaches(classes, module, runAsBehavior); + + return result; + } + + private static List ProcessEmbeddedInstanceTypes(List embeddedInstanceTypes, List visitedInstances) + { + var result = new List(); + while (embeddedInstanceTypes.Count > 0) + { + var batchedTypes = embeddedInstanceTypes.Where(x => !visitedInstances.Contains(x)).ToArray(); + embeddedInstanceTypes.Clear(); + + for (int i = batchedTypes.Length - 1; i >= 0; i--) + { + visitedInstances.Add(batchedTypes[i]); + var typeAst = batchedTypes[i] as TypeDefinitionAst; + if (typeAst is not null) + { + var classes = GenerateJsonClassesForAst(typeAst, embeddedInstanceTypes); + result.AddRange(classes); + } + } + } + + return result; + } + + private static void AddEmbeddedInstanceTypesToCaches(IEnumerable classes, PSModuleInfo module, DSCResourceRunAsCredential runAsBehavior) + { + foreach (dynamic c in classes) + { + var className = c.ClassName; + string alias = GetFriendlyName(c); + var friendlyName = string.IsNullOrEmpty(alias) ? className : alias; + var moduleQualifiedResourceName = GetModuleQualifiedResourceName(module.Name, module.Version.ToString(), className, friendlyName); + var classCacheEntry = new DscClassCacheEntry(runAsBehavior, false, c, module.Path); + ClassCache[moduleQualifiedResourceName] = classCacheEntry; + GuestConfigClassCache[moduleQualifiedResourceName] = classCacheEntry; + ByClassModuleCache[className] = new Tuple(module.Name, module.Version); + } + } + + internal static string MapTypeNameToMofType(ITypeName typeName, string memberName, string className, out bool isArrayType, out string embeddedInstanceType, List embeddedInstanceTypes, ref string[] enumNames) + { + TypeName propTypeName; + var arrayTypeName = typeName as ArrayTypeName; + if (arrayTypeName is not null) + { + isArrayType = true; + propTypeName = arrayTypeName.ElementType as TypeName; + } + else + { + isArrayType = false; + propTypeName = typeName as TypeName; + } + + if (propTypeName is null || propTypeName._typeDefinitionAst is null) + { + throw new NotSupportedException(string.Format( + CultureInfo.InvariantCulture, + ParserStrings.UnsupportedPropertyTypeOfDSCResourceClass, + memberName, + typeName.FullName, + typeName)); + } + + if (propTypeName._typeDefinitionAst.IsEnum) + { + enumNames = propTypeName._typeDefinitionAst.Members.Select(m => m.Name).ToArray(); + isArrayType = false; + embeddedInstanceType = null; + return "string"; + } + + if (!embeddedInstanceTypes.Contains(propTypeName._typeDefinitionAst)) + { + embeddedInstanceTypes.Add(propTypeName._typeDefinitionAst); + } + + embeddedInstanceType = propTypeName.Name.Replace('.', '_'); + return "Instance"; + } + + private static PSObject[] GenerateJsonClassesForAst(TypeDefinitionAst typeAst, List embeddedInstanceTypes) + { + // MOF-based implementation of this used to generate MOF string representing classes/typeAst and pass it to MMI/MOF deserializer to get CimClass array + // Here we are avoiding that roundtrip by constructing the resulting PSObjects directly + var className = typeAst.Name; + + string cimSuperClassName = null; + if (typeAst.Attributes.Any(a => a.TypeName.GetReflectionAttributeType() == typeof(DscResourceAttribute))) + { + cimSuperClassName = "OMI_BaseResource"; + } + + var cimClassProperties = ProcessMembers(embeddedInstanceTypes, typeAst, className).ToArray(); + + Queue bases = new Queue(); + foreach (var b in typeAst.BaseTypes) + { + bases.Enqueue(b); + } + + while (bases.Count > 0) + { + var b = bases.Dequeue(); + var tc = b as TypeConstraintAst; + + if (tc is not null) + { + b = tc.TypeName.GetReflectionType(); + if (b is null) + { + var td = tc.TypeName as TypeName; + if (td is not null && td._typeDefinitionAst is not null) + { + ProcessMembers(embeddedInstanceTypes, td._typeDefinitionAst, className); + foreach (var b1 in td._typeDefinitionAst.BaseTypes) + { + bases.Enqueue(b1); + } + } + + continue; + } + } + } + + var result = new PSObject(); + result.Properties.Add(new PSNoteProperty("ClassName", className)); + result.Properties.Add(new PSNoteProperty("FriendlyName", className)); + result.Properties.Add(new PSNoteProperty("SuperClassName", cimSuperClassName)); + result.Properties.Add(new PSNoteProperty("ClassProperties", cimClassProperties)); + + return new[] { result }; + } + + private static List ProcessMembers(List embeddedInstanceTypes, TypeDefinitionAst typeDefinitionAst, string className) + { + List result = new List(); + + foreach (var member in typeDefinitionAst.Members) + { + var property = member as PropertyMemberAst; + + if (property == null || property.IsStatic || + property.Attributes.All(a => a.TypeName.GetReflectionAttributeType() != typeof(DscPropertyAttribute))) + { + continue; + } + + var memberType = property.PropertyType is null + ? typeof(object) + : property.PropertyType.TypeName.GetReflectionType(); + + var attributes = new List(); + for (int i = 0; i < property.Attributes.Count; i++) + { + attributes.Add(property.Attributes[i].GetAttribute()); + } + + string mofType; + bool isArrayType; + string embeddedInstanceType; + string[] enumNames = null; + + if (memberType != null) + { + mofType = MapTypeToMofType(memberType, member.Name, className, out isArrayType, out embeddedInstanceType, embeddedInstanceTypes); + if (memberType.IsEnum) + { + enumNames = Enum.GetNames(memberType); + } + } + else + { + // PropertyType can't be null, we used typeof(object) above in that case so we don't get here. + mofType = MapTypeNameToMofType(property.PropertyType.TypeName, member.Name, className, out isArrayType, out embeddedInstanceType, embeddedInstanceTypes, ref enumNames); + } + + var propertyObject = new PSObject(); + propertyObject.Properties.Add(new PSNoteProperty(@"Name", member.Name)); + propertyObject.Properties.Add(new PSNoteProperty(@"CimType", mofType + (isArrayType ? "Array" : string.Empty))); + if (!string.IsNullOrEmpty(embeddedInstanceType)) + { + propertyObject.Properties.Add(new PSNoteProperty(@"ReferenceClassName", embeddedInstanceType)); + } + + PSObject attributesPSObject = null; + foreach (var attr in attributes) + { + var dscProperty = attr as DscPropertyAttribute; + if (dscProperty is not null) + { + if (attributesPSObject is null) + { + attributesPSObject = new PSObject(); + } + + if (dscProperty.Key) + { + attributesPSObject.Properties.Add(new PSNoteProperty("Key", true)); + } + + if (dscProperty.Mandatory) + { + attributesPSObject.Properties.Add(new PSNoteProperty("Required", true)); + } + + if (dscProperty.NotConfigurable) + { + attributesPSObject.Properties.Add(new PSNoteProperty("Read", true)); + } + + continue; + } + + var validateSet = attr as ValidateSetAttribute; + if (validateSet is not null) + { + if (attributesPSObject is null) + { + attributesPSObject = new PSObject(); + } + + List valueMap = new List(validateSet.ValidValues); + List values = new List(validateSet.ValidValues); + attributesPSObject.Properties.Add(new PSNoteProperty("ValueMap", valueMap)); + attributesPSObject.Properties.Add(new PSNoteProperty("Values", values)); + } + } + + if (attributesPSObject is not null) + { + propertyObject.Properties.Add(new PSNoteProperty(@"Qualifiers", attributesPSObject)); + } + + result.Add(propertyObject); + } + + return result; + } + + private static bool GetResourceDefinitionsFromModule(string fileName, out IEnumerable resourceDefinitions, Collection errorList, IScriptExtent extent) + { + resourceDefinitions = null; + + if (string.IsNullOrEmpty(fileName)) + { + return false; + } + + if (!".psm1".Equals(Path.GetExtension(fileName), StringComparison.OrdinalIgnoreCase) && + !".ps1".Equals(Path.GetExtension(fileName), StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + Token[] tokens; + ParseError[] errors; + var ast = Parser.ParseFile(fileName, out tokens, out errors); + + if (errors is not null && errors.Length > 0) + { + if (errorList is not null && extent is not null) + { + List errorMessages = new List(); + foreach (var error in errors) + { + errorMessages.Add(error.ToString()); + } + + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.FailToParseModuleScriptFile, fileName, string.Join(Environment.NewLine, errorMessages)); + e.SetErrorId("FailToParseModuleScriptFile"); + errorList.Add(e); + } + + return false; + } + + resourceDefinitions = ast.FindAll( + n => + { + var typeAst = n as TypeDefinitionAst; + if (typeAst is not null) + { + for (int i = 0; i < typeAst.Attributes.Count; i++) + { + var a = typeAst.Attributes[i]; + if (a.TypeName.GetReflectionAttributeType() == typeof(DscResourceAttribute)) + { + return true; + } + } + } + + return false; + }, + false); + + return true; + } + + private static bool LoadPowerShellClassResourcesFromModule(string fileName, PSModuleInfo module, ICollection resourcesToImport, ICollection resourcesFound, Dictionary functionsToDefine, Collection errorList, IScriptExtent extent) + { + IEnumerable resourceDefinitions; + if (!GetResourceDefinitionsFromModule(fileName, out resourceDefinitions, errorList, extent)) + { + return false; + } + + var result = false; + + const WildcardOptions options = WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant; + IEnumerable patternList = SessionStateUtilities.CreateWildcardsFromStrings(module._declaredDscResourceExports, options); + + foreach (var r in resourceDefinitions) + { + result = true; + var resourceDefnAst = (TypeDefinitionAst)r; + + if (!SessionStateUtilities.MatchesAnyWildcardPattern(resourceDefnAst.Name, patternList, true)) + { + continue; + } + + bool skip = true; + foreach (var toImport in resourcesToImport) + { + if (WildcardPattern.Get(toImport, WildcardOptions.IgnoreCase).IsMatch(resourceDefnAst.Name)) + { + skip = false; + break; + } + } + + if (skip) + { + continue; + } + + // Parse the Resource Attribute to see if RunAs behavior is specified for the resource. + DSCResourceRunAsCredential runAsBehavior = DSCResourceRunAsCredential.Default; + foreach (var attr in resourceDefnAst.Attributes) + { + if (attr.TypeName.GetReflectionAttributeType() == typeof(DscResourceAttribute)) + { + foreach (var na in attr.NamedArguments) + { + if (na.ArgumentName.Equals("RunAsCredential", StringComparison.OrdinalIgnoreCase)) + { + var dscResourceAttribute = attr.GetAttribute() as DscResourceAttribute; + if (dscResourceAttribute != null) + { + runAsBehavior = dscResourceAttribute.RunAsCredential; + } + } + } + } + } + + var classes = GenerateJsonClassesForAst(resourceDefnAst, module, runAsBehavior); + + ProcessJsonForDynamicKeywords(module, resourcesFound, functionsToDefine, classes, runAsBehavior, errorList); + } + + return result; + } + + private static readonly Dictionary s_mapPrimitiveDotNetTypeToMof = new Dictionary() + { + { typeof(sbyte), "sint8" }, + { typeof(byte), "uint8" }, + { typeof(short), "sint16" }, + { typeof(ushort), "uint16" }, + { typeof(int), "sint32" }, + { typeof(uint), "uint32" }, + { typeof(long), "sint64" }, + { typeof(ulong), "uint64" }, + { typeof(float), "real32" }, + { typeof(double), "real64" }, + { typeof(bool), "boolean" }, + { typeof(string), "string" }, + { typeof(DateTime), "datetime" }, + { typeof(PSCredential), "string" }, + { typeof(char), "char16" }, + }; + + internal static string MapTypeToMofType(Type type, string memberName, string className, out bool isArrayType, out string embeddedInstanceType, List embeddedInstanceTypes) + { + isArrayType = false; + if (type.IsValueType) + { + type = Nullable.GetUnderlyingType(type) ?? type; + } + + if (type.IsEnum) + { + embeddedInstanceType = null; + return "string"; + } + + if (type == typeof(Hashtable)) + { + // Hashtable is obviously not an array, but in the mof, we represent + // it as string[] (really, embeddedinstance of MSFT_KeyValuePair), but + // we need an array to hold each entry in the hashtable. + isArrayType = true; + embeddedInstanceType = "MSFT_KeyValuePair"; + return "string"; + } + + if (type == typeof(PSCredential)) + { + embeddedInstanceType = "MSFT_Credential"; + return "string"; + } + + if (type.IsArray) + { + isArrayType = true; + bool temp; + var elementType = type.GetElementType(); + if (!elementType.IsArray) + { + return MapTypeToMofType(type.GetElementType(), memberName, className, out temp, out embeddedInstanceType, embeddedInstanceTypes); + } + } + else + { + string cimType; + if (s_mapPrimitiveDotNetTypeToMof.TryGetValue(type, out cimType)) + { + embeddedInstanceType = null; + return cimType; + } + } + + bool supported = false; + bool missingDefaultConstructor = false; + if (type.IsValueType) + { + if (s_mapPrimitiveDotNetTypeToMof.ContainsKey(type)) + { + supported = true; + } + } + else if (!type.IsAbstract) + { + // Must have default constructor, at least 1 public property/field, and no base classes + if (type.GetConstructor(Type.EmptyTypes) is null) + { + missingDefaultConstructor = true; + } + else if (type.BaseType == typeof(object) && + (type.GetProperties(BindingFlags.Instance | BindingFlags.Public).Length > 0 || + type.GetFields(BindingFlags.Instance | BindingFlags.Public).Length > 0)) + { + supported = true; + } + } + + if (supported) + { + if (!embeddedInstanceTypes.Contains(type)) + { + embeddedInstanceTypes.Add(type); + } + + // The type is obviously not a string, but in the mof, we represent + // it as string (really, embeddedinstance of the class type) + embeddedInstanceType = type.FullName.Replace('.', '_'); + return "string"; + } + + if (missingDefaultConstructor) + { + throw new NotSupportedException(string.Format( + CultureInfo.InvariantCulture, + ParserStrings.DscResourceMissingDefaultConstructor, + type.Name)); + } + else + { + throw new NotSupportedException(string.Format( + CultureInfo.InvariantCulture, + ParserStrings.UnsupportedPropertyTypeOfDSCResourceClass, + memberName, + type.Name, + className)); + } + } + + private static void ProcessJsonForDynamicKeywords( + PSModuleInfo module, + ICollection resourcesFound, + Dictionary functionsToDefine, + PSObject[] classes, + DSCResourceRunAsCredential runAsBehavior, + Collection errors) + { + foreach (dynamic c in classes) + { + var className = c.ClassName; + string alias = GetFriendlyName(c); + var friendlyName = string.IsNullOrEmpty(alias) ? className : alias; + if (!CacheResourcesFromMultipleModuleVersions) + { + // Find & remove the previous version of the resource. + List> resourceList = FindResourceInCache(module.Name, className, friendlyName); + + if (resourceList.Count > 0 && !string.IsNullOrEmpty(resourceList[0].Key)) + { + ClassCache.Remove(resourceList[0].Key); + + // keyword is already defined and it is a Inbox resource, remove it + if (DynamicKeyword.ContainsKeyword(friendlyName) && resourceList[0].Value.IsImportedImplicitly) + { + DynamicKeyword.RemoveKeyword(friendlyName); + } + } + } + + var moduleQualifiedResourceName = GetModuleQualifiedResourceName(module.Name, module.Version.ToString(), className, friendlyName); + DscClassCacheEntry existingCacheEntry = null; + if (ClassCache.TryGetValue(moduleQualifiedResourceName, out existingCacheEntry)) + { + if (errors is not null) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.DuplicateCimClassDefinition, className, module.Path, existingCacheEntry.ModulePath); + e.SetErrorId("DuplicateCimClassDefinition"); + errors.Add(e); + } + } + else + { + var classCacheEntry = new DscClassCacheEntry(runAsBehavior, false, c, module.Path); + ClassCache[moduleQualifiedResourceName] = classCacheEntry; + GuestConfigClassCache[moduleQualifiedResourceName] = classCacheEntry; + ByClassModuleCache[className] = new Tuple(module.Name, module.Version); + resourcesFound.Add(className); + CreateAndRegisterKeywordFromCimClass(module.Name, module.Version, c, functionsToDefine, runAsBehavior); + } + } + } + + /// + /// Returns an error record to use in the case of a malformed resource reference in the DependsOn list. + /// + /// The malformed resource. + /// The referencing resource instance. + /// Generated error record. + public static ErrorRecord GetBadlyFormedRequiredResourceIdErrorRecord(string badDependsOnReference, string definingResource) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.GetBadlyFormedRequiredResourceId, badDependsOnReference, definingResource); + e.SetErrorId("GetBadlyFormedRequiredResourceId"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of a malformed resource reference in the exclusive resources list. + /// + /// The malformed resource. + /// The referencing resource instance. + /// Generated error record. + public static ErrorRecord GetBadlyFormedExclusiveResourceIdErrorRecord(string badExclusiveResourcereference, string definingResource) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.GetBadlyFormedExclusiveResourceId, badExclusiveResourcereference, definingResource); + e.SetErrorId("GetBadlyFormedExclusiveResourceId"); + return e.ErrorRecord; + } + + /// + /// If a partial configuration is in 'Pull' Mode, it needs a configuration source. + /// + /// Resource id. + /// Generated error record. + public static ErrorRecord GetPullModeNeedConfigurationSource(string resourceId) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.GetPullModeNeedConfigurationSource, resourceId); + e.SetErrorId("GetPullModeNeedConfigurationSource"); + return e.ErrorRecord; + } + + /// + /// Refresh Mode can not be Disabled for the Partial Configurations. + /// + /// Resource id. + /// Generated error record. + public static ErrorRecord DisabledRefreshModeNotValidForPartialConfig(string resourceId) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.DisabledRefreshModeNotValidForPartialConfig, resourceId); + e.SetErrorId("DisabledRefreshModeNotValidForPartialConfig"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of a malformed resource reference in the DependsOn list. + /// + /// The duplicate resource identifier. + /// The node being defined. + /// The error record to use. + public static ErrorRecord DuplicateResourceIdInNodeStatementErrorRecord(string duplicateResourceId, string nodeName) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.DuplicateResourceIdInNodeStatement, duplicateResourceId, nodeName); + e.SetErrorId("DuplicateResourceIdInNodeStatement"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of a configuration name is invalid. + /// + /// Configuration name. + /// Generated error record. + public static ErrorRecord InvalidConfigurationNameErrorRecord(string configurationName) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.InvalidConfigurationName, configurationName); + e.SetErrorId("InvalidConfigurationName"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of the given value for a property is invalid. + /// + /// Property name. + /// Property value. + /// Keyword name. + /// Valid property values. + /// Generated error record. + public static ErrorRecord InvalidValueForPropertyErrorRecord(string propertyName, string value, string keywordName, string validValues) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.InvalidValueForProperty, value, propertyName, keywordName, validValues); + e.SetErrorId("InvalidValueForProperty"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in case the given property is not valid LocalConfigurationManager property. + /// + /// Property name. + /// Valid properties. + /// Generated error record. + public static ErrorRecord InvalidLocalConfigurationManagerPropertyErrorRecord(string propertyName, string validProperties) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.InvalidLocalConfigurationManagerProperty, propertyName, validProperties); + e.SetErrorId("InvalidLocalConfigurationManagerProperty"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of the given value for a property is not supported. + /// + /// Property name. + /// Property value. + /// Keyword name. + /// Valid property values. + /// Generated error record. + public static ErrorRecord UnsupportedValueForPropertyErrorRecord(string propertyName, string value, string keywordName, string validValues) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.UnsupportedValueForProperty, value, propertyName, keywordName, validValues); + e.SetErrorId("UnsupportedValueForProperty"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of no value is provided for a mandatory property. + /// + /// Keyword name. + /// Type name. + /// Property name. + /// Generated error record. + public static ErrorRecord MissingValueForMandatoryPropertyErrorRecord(string keywordName, string typeName, string propertyName) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.MissingValueForMandatoryProperty, keywordName, typeName, propertyName); + e.SetErrorId("MissingValueForMandatoryProperty"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of more than one values are provided for DebugMode property. + /// + /// Generated error record. + public static ErrorRecord DebugModeShouldHaveOneValue() + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.DebugModeShouldHaveOneValue); + e.SetErrorId("DebugModeShouldHaveOneValue"); + return e.ErrorRecord; + } + + /// + /// Return an error to indicate a value is out of range for a dynamic keyword property. + /// + /// Rroperty name. + /// Resource name. + /// Provided value. + /// Valid range lower bound. + /// Valid range upper bound. + /// Generated error record. + public static ErrorRecord ValueNotInRangeErrorRecord(string property, string name, int providedValue, int lower, int upper) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.ValueNotInRange, property, name, providedValue, lower, upper); + e.SetErrorId("ValueNotInRange"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use when composite resource and its resource instances both has PsDscRunAsCredentials value. + /// + /// ResourceId of resource. + /// Generated error record. + public static ErrorRecord PsDscRunAsCredentialMergeErrorForCompositeResources(string resourceId) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.PsDscRunAsCredentialMergeErrorForCompositeResources, resourceId); + e.SetErrorId("PsDscRunAsCredentialMergeErrorForCompositeResources"); + return e.ErrorRecord; + } + + /// + /// Routine to format a usage string from keyword. The resulting string should look like: + /// User [string] #ResourceName + /// { + /// UserName = [string] + /// [ Description = [string] ] + /// [ Disabled = [bool] ] + /// [ Ensure = [string] { Absent | Present } ] + /// [ Force = [bool] ] + /// [ FullName = [string] ] + /// [ Password = [PSCredential] ] + /// [ PasswordChangeNotAllowed = [bool] ] + /// [ PasswordChangeRequired = [bool] ] + /// [ PasswordNeverExpires = [bool] ] + /// [ DependsOn = [string[]] ] + /// } + /// + /// Dynamic keyword. + /// Usage string. + public static string GetDSCResourceUsageString(DynamicKeyword keyword) + { + StringBuilder usageString; + switch (keyword.NameMode) + { + // Name must be present and simple non-empty bare word + case DynamicKeywordNameMode.SimpleNameRequired: + usageString = new StringBuilder(keyword.Keyword + " [string] # Resource Name"); + break; + + // Name must be present but can also be an expression + case DynamicKeywordNameMode.NameRequired: + usageString = new StringBuilder(keyword.Keyword + " [string[]] # Name List"); + break; + + // Name may be optionally present, but if it is present, it must be a non-empty bare word. + case DynamicKeywordNameMode.SimpleOptionalName: + usageString = new StringBuilder(keyword.Keyword + " [ [string] ] # Optional Name"); + break; + + // Name may be optionally present, expression or bare word + case DynamicKeywordNameMode.OptionalName: + usageString = new StringBuilder(keyword.Keyword + " [ [string[]] ] # Optional NameList"); + break; + + // Does not take a name + default: + usageString = new StringBuilder(keyword.Keyword); + break; + } + + usageString.Append("\n{\n"); + + bool listKeyProperties = true; + while (true) + { + foreach (var prop in keyword.Properties.OrderBy(ob => ob.Key)) + { + if (string.Equals(prop.Key, "ResourceId", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var propVal = prop.Value; + if ((listKeyProperties && propVal.IsKey) || (!listKeyProperties && !propVal.IsKey)) + { + usageString.Append(propVal.Mandatory ? " " : " [ "); + usageString.Append(prop.Key); + usageString.Append(" = "); + usageString.Append(FormatCimPropertyType(propVal, !propVal.Mandatory)); + } + } + + if (listKeyProperties) + { + listKeyProperties = false; + } + else + { + break; + } + } + + usageString.Append('}'); + + return usageString.ToString(); + } + + /// + /// Format the type name of a CIM property in a presentable way. + /// + /// Dynamic keyword property. + /// If this is optional property or not. + /// CIM property type string. + private static StringBuilder FormatCimPropertyType(DynamicKeywordProperty prop, bool isOptionalProperty) + { + string cimTypeName = prop.TypeConstraint; + StringBuilder formattedTypeString = new StringBuilder(); + + if (string.Equals(cimTypeName, "MSFT_Credential", StringComparison.OrdinalIgnoreCase)) + { + formattedTypeString.Append("[PSCredential]"); + } + else if (string.Equals(cimTypeName, "MSFT_KeyValuePair", StringComparison.OrdinalIgnoreCase) || string.Equals(cimTypeName, "MSFT_KeyValuePair[]", StringComparison.OrdinalIgnoreCase)) + { + formattedTypeString.Append("[Hashtable]"); + } + else + { + string convertedTypeString = System.Management.Automation.LanguagePrimitives.ConvertTypeNameToPSTypeName(cimTypeName); + if (!string.IsNullOrEmpty(convertedTypeString) && !string.Equals(convertedTypeString, "[]", StringComparison.OrdinalIgnoreCase)) + { + formattedTypeString.Append(convertedTypeString); + } + else + { + formattedTypeString.Append("[" + cimTypeName + "]"); + } + } + + // Do the property values map + if (prop.ValueMap is not null && prop.ValueMap.Count > 0) + { + formattedTypeString.Append(" { " + string.Join(" | ", prop.ValueMap.Keys.OrderBy(x => x)) + " }"); + } + + // We prepend optional property with "[" so close out it here. This way it is shown with [ ] to indication optional + if (isOptionalProperty) + { + formattedTypeString.Append(']'); + } + + formattedTypeString.Append('\n'); + + return formattedTypeString; + } + + /// + /// Gets the scriptblock that implements the CIM keyword functionality. + /// + private static ScriptBlock CimKeywordImplementationFunction + { + get + { + // The scriptblock cache will handle mutual exclusion + return s_cimKeywordImplementationFunction ??= ScriptBlock.Create(CimKeywordImplementationFunctionText); + } + } + + private static ScriptBlock s_cimKeywordImplementationFunction; + + private const string CimKeywordImplementationFunctionText = @" + param ( + [Parameter(Mandatory)] + $KeywordData, + [Parameter(Mandatory)] + $Name, + [Parameter(Mandatory)] + [Hashtable] + $Value, + [Parameter(Mandatory)] + $SourceMetadata + ) + +# walk the call stack to get at all of the enclosing configuration resource IDs + $stackedConfigs = @(Get-PSCallStack | + where { ($null -ne $_.InvocationInfo.MyCommand) -and ($_.InvocationInfo.MyCommand.CommandType -eq 'Configuration') }) +# keep all but the top-most + $stackedConfigs = $stackedConfigs[0..(@($stackedConfigs).Length - 2)] +# and build the complex resource ID suffix. + $complexResourceQualifier = ( $stackedConfigs | ForEach-Object { '[' + $_.Command + ']' + $_.InvocationInfo.BoundParameters['InstanceName'] } ) -join '::' + +# +# Utility function used to validate that the DependsOn arguments are well-formed. +# The function also adds them to the define nodes resource collection. +# in the case of resources generated inside a script resource, this routine +# will also fix up the DependsOn references to '[Type]Instance::[OuterType]::OuterInstance +# + function Test-DependsOn + { + +# make sure the references are well-formed + $updatedDependsOn = foreach ($DependsOnVar in $value['DependsOn']) { +# match [ResourceType]ResourceName. ResourceName should starts with [a-z_0-9] followed by [a-z_0-9\p{Zs}\.\\-]* + if ($DependsOnVar -notmatch '^\[[a-z]\w*\][a-z_0-9][a-z_0-9\p{Zs}\.\\-]*$') + { + Update-ConfigurationErrorCount + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::GetBadlyFormedRequiredResourceIdErrorRecord($DependsOnVar, $resourceId)) + } + +# Fix up DependsOn for nested names + if ($MyTypeName -and $typeName -ne $MyTypeName -and $InstanceName) + { + ""$DependsOnVar::$complexResourceQualifier"" + } + else + { + $DependsOnVar + } + } + + $value['DependsOn']= $updatedDependsOn + + if($null -ne $DependsOn) + { +# +# Combine DependsOn with dependson from outer composite resource +# which is set as local variable $DependsOn at the composite resource context +# + $value['DependsOn']= @($value['DependsOn']) + $DependsOn + } + +# Save the resource id in a per-node dictionary to do cross validation at the end + Set-NodeResources $resourceId @( $value['DependsOn']) + +# Remove depends on because it need to be fixed up for composite resources +# We do it in ValidateNodeResource and Update-Depends on in configuration/Node function + $value.Remove('DependsOn') + } + +# A copy of the value object with correctly-cased property names + $canonicalizedValue = @{} + + $typeName = $keywordData.ResourceName # CIM type + $keywordName = $keywordData.Keyword # user-friendly alias that is used in scripts + $keyValues = '' + $debugPrefix = "" ${TypeName}:"" # set up a debug prefix string that makes it easier to track what's happening. + + Write-Debug ""${debugPrefix} RESOURCE PROCESSING STARTED [KeywordName='$keywordName'] Function='$($myinvocation.Invocationname)']"" + +# Check whether it's an old style metaconfig + $OldMetaConfig = $false + if ((-not $IsMetaConfig) -and ($keywordName -ieq 'LocalConfigurationManager')) { + $OldMetaConfig = $true + } + +# Check to see if it's a resource keyword. If so add the meta-properties to the canonical property collection. + $resourceId = $null +# todo: need to include configuration managers and partial configuration + if (($keywordData.Properties.Keys -contains 'DependsOn') -or (($KeywordData.ImplementingModule -ieq 'PSDesiredStateConfigurationEngine') -and ($KeywordData.NameMode -eq [System.Management.Automation.Language.DynamicKeywordNameMode]::NameRequired))) + { + + $resourceId = ""[$keywordName]$name"" + if ($MyTypeName -and $keywordName -ne $MyTypeName -and $InstanceName) + { + $resourceId += ""::$complexResourceQualifier"" + } + + Write-Debug ""${debugPrefix} ResourceID = $resourceId"" + +# copy the meta-properties + $canonicalizedValue['ResourceID'] = $resourceId + $canonicalizedValue['SourceInfo'] = $SourceMetadata + if(-not $IsMetaConfig) + { + $canonicalizedValue['ModuleName'] = $keywordData.ImplementingModule + $canonicalizedValue['ModuleVersion'] = $keywordData.ImplementingModuleVersion -as [string] + } + +# see if there is already a resource with this ID. + if (Test-NodeResources $resourceId) + { + Update-ConfigurationErrorCount + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::DuplicateResourceIdInNodeStatementErrorRecord($resourceId, (Get-PSCurrentConfigurationNode))) + } + else + { +# If there are prerequisite resources, validate that the references are well-formed strings +# This routine also adds the resource to the global node resources table. + Test-DependsOn + +# Check if PsDscRunCredential is being specified as Arguments to Configuration + if($null -ne $PsDscRunAsCredential) + { +# Check if resource is also trying to set the value for RunAsCred +# In that case we will generate error during compilation, this is merge error + if($null -ne $value['PsDscRunAsCredential']) + { + Update-ConfigurationErrorCount + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::PsDscRunAsCredentialMergeErrorForCompositeResources($resourceId)) + } +# Set the Value of RunAsCred to that of outer configuration + else + { + $value['PsDscRunAsCredential'] = $PsDscRunAsCredential + } + } + +# Save the resource id in a per-node dictionary to do cross validation at the end + if($keywordData.ImplementingModule -ieq ""PSDesiredStateConfigurationEngine"") + { +#$keywordName is PartialConfiguration + if($keywordName -eq 'PartialConfiguration') + { +# RefreshMode is 'Pull' and .ConfigurationSource is empty + if($value['RefreshMode'] -eq 'Pull' -and -not $value['ConfigurationSource']) + { + Update-ConfigurationErrorCount + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::GetPullModeNeedConfigurationSource($resourceId)) + } + +# Verify that RefreshMode is not Disabled for Partial configuration + if($value['RefreshMode'] -eq 'Disabled') + { + Update-ConfigurationErrorCount + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::DisabledRefreshModeNotValidForPartialConfig($resourceId)) + } + + if($null -ne $value['ConfigurationSource']) + { + Set-NodeManager $resourceId $value['ConfigurationSource'] + } + + if($null -ne $value['ResourceModuleSource']) + { + Set-NodeResourceSource $resourceId $value['ResourceModuleSource'] + } + } + + if($null -ne $value['ExclusiveResources']) + { +# make sure the references are well-formed + foreach ($ExclusiveResource in $value['ExclusiveResources']) { + if (($ExclusiveResource -notmatch '^[a-z][a-z_0-9]*\\[a-z][a-z_0-9]*$') -and ($ExclusiveResource -notmatch '^[a-z][a-z_0-9]*$') -and ($ExclusiveResource -notmatch '^[a-z][a-z_0-9]*\\\*$')) + { + Update-ConfigurationErrorCount + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::GetBadlyFormedExclusiveResourceIdErrorRecord($ExclusiveResource, $resourceId)) + } + } + +# Save the resource id in a per-node dictionary to do cross validation at the end +# Validate resource exist +# Also update the resource reference from module\friendlyname to module\name + $value['ExclusiveResources'] = @(Set-NodeExclusiveResources $resourceId @( $value['ExclusiveResources'] )) + } + } + } + } + else + { + Write-Debug ""${debugPrefix} TYPE IS NOT AS DSC RESOURCE"" + } + +# +# Copy the user-supplied values into a new collection with canonicalized property names +# + foreach ($key in $keywordData.Properties.Keys) + { + Write-Debug ""${debugPrefix} Processing property '$key' ["" + + if ($value.Contains($key)) + { + if ($OldMetaConfig -and (-not ($V1MetaConfigPropertyList -contains $key))) + { + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::InvalidLocalConfigurationManagerPropertyErrorRecord($key, ($V1MetaConfigPropertyList -join ', '))) + Update-ConfigurationErrorCount + } +# see if there is a list of allowed values for this property (similar to an enum) + $allowedValues = $keywordData.Properties[$key].Values +# If there is and user-provided value is not in that list, write an error. + if ($allowedValues) + { + if(($null -eq $value[$key]) -and ($allowedValues -notcontains $value[$key])) + { + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::InvalidValueForPropertyErrorRecord($key, ""$($value[$key])"", $keywordData.Keyword, ($allowedValues -join ', '))) + Update-ConfigurationErrorCount + } + else + { + $notAllowedValue=$null + foreach($v in $value[$key]) + { + if($allowedValues -notcontains $v) + { + $notAllowedValue +=$v.ToString() + ', ' + } + } + + if($notAllowedValue) + { + $notAllowedValue = $notAllowedValue.Substring(0, $notAllowedValue.Length -2) + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::UnsupportedValueForPropertyErrorRecord($key, $notAllowedValue, $keywordData.Keyword, ($allowedValues -join ', '))) + Update-ConfigurationErrorCount + } + } + } + +# see if a value range is defined for this property + $allowedRange = $keywordData.Properties[$key].Range + if($allowedRange) + { + $castedValue = $value[$key] -as [int] + if((($castedValue -is [int]) -and (($castedValue -lt $keywordData.Properties[$key].Range.Item1) -or ($castedValue -gt $keywordData.Properties[$key].Range.Item2))) -or ($null -eq $castedValue)) + { + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::ValueNotInRangeErrorRecord($key, $keywordName, $value[$key], $keywordData.Properties[$key].Range.Item1, $keywordData.Properties[$key].Range.Item2)) + Update-ConfigurationErrorCount + } + } + + Write-Debug ""${debugPrefix} Canonicalized property '$key' = '$($value[$key])'"" + + if ($keywordData.Properties[$key].IsKey) + { + if($null -eq $value[$key]) + { + $keyValues += ""::__NULL__"" + } + else + { + $keyValues += ""::"" + $value[$key] + } + } + +# see if ValueMap is also defined for this property (actual values) + $allowedValueMap = $keywordData.Properties[$key].ValueMap +#if it is and the ValueMap contains the user-provided value as a key, use the actual value + if ($allowedValueMap -and $allowedValueMap.ContainsKey($value[$key])) + { + $canonicalizedValue[$key] = $allowedValueMap[$value[$key]] + } + else + { + $canonicalizedValue[$key] = $value[$key] + } + } + elseif ($keywordData.Properties[$key].Mandatory) + { +# If the property was mandatory but the user didn't provide a value, write and error. + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::MissingValueForMandatoryPropertyErrorRecord($keywordData.Keyword, $keywordData.Properties[$key].TypeConstraint, $Key)) + Update-ConfigurationErrorCount + } + + Write-Debug ""${debugPrefix} Processing completed '$key' ]"" + } + + if($keyValues) + { + $keyValues = $keyValues.Substring(2) # Remove the leading '::' + Add-NodeKeys $keyValues $keywordName + Test-ConflictingResources $keywordName $canonicalizedValue $keywordData + } + +# update OMI_ConfigurationDocument + if($IsMetaConfig) + { + if($keywordData.ResourceName -eq 'OMI_ConfigurationDocument') + { + if($(Get-PSMetaConfigurationProcessed)) + { + $PSMetaConfigDocumentInstVersionInfo = Get-PSMetaConfigDocumentInstVersionInfo + $canonicalizedValue['MinimumCompatibleVersion']=$PSMetaConfigDocumentInstVersionInfo['MinimumCompatibleVersion'] + } + else + { + Set-PSMetaConfigDocInsProcessedBeforeMeta + $canonicalizedValue['MinimumCompatibleVersion']='1.0.0' + } + } + + if(($keywordData.ResourceName -eq 'MSFT_WebDownloadManager') ` + -or ($keywordData.ResourceName -eq 'MSFT_FileDownloadManager') ` + -or ($keywordData.ResourceName -eq 'MSFT_WebResourceManager') ` + -or ($keywordData.ResourceName -eq 'MSFT_FileResourceManager') ` + -or ($keywordData.ResourceName -eq 'MSFT_WebReportManager') ` + -or ($keywordData.ResourceName -eq 'MSFT_SignatureValidation') ` + -or ($keywordData.ResourceName -eq 'MSFT_PartialConfiguration')) + { + Set-PSMetaConfigVersionInfoV2 + } + } + elseif($keywordData.ResourceName -eq 'OMI_ConfigurationDocument') + { + $canonicalizedValue['MinimumCompatibleVersion']='1.0.0' + $canonicalizedValue['CompatibleVersionAdditionalProperties']=@('Omi_BaseResource:ConfigurationName') + } + + if(($keywordData.ResourceName -eq 'MSFT_DSCMetaConfiguration') -or ($keywordData.ResourceName -eq 'MSFT_DSCMetaConfigurationV2')) + { + if($canonicalizedValue['DebugMode'] -and @($canonicalizedValue['DebugMode']).Length -gt 1) + { +# we only allow one value for debug mode now. + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::DebugModeShouldHaveOneValue()) + Update-ConfigurationErrorCount + } + } + +# Generate the MOF text for this resource instance. +# when generate mof text for OMI_ConfigurationDocument we handle below two cases: +# 1. we will add versioning related property based on meta configuration instance already process +# 2. we update the existing OMI_ConfigurationDocument instance if it already exists when process meta configuration instance + $aliasId = ConvertTo-MOFInstance $keywordName $canonicalizedValue + +# If a OMI_ConfigurationDocument is executed outside of a node statement, it becomes the default +# for all nodes that don't have an explicit OMI_ConfigurationDocument declaration + if ($keywordData.ResourceName -eq 'OMI_ConfigurationDocument' -and -not (Get-PSCurrentConfigurationNode)) + { + $data = Get-MoFInstanceText $aliasId + Write-Debug ""${debugPrefix} DEFINING DEFAULT CONFIGURATION DOCUMENT: $data"" + Set-PSDefaultConfigurationDocument $data + } + + Write-Debug ""${debugPrefix} MOF alias for this resource is '$aliasId'"" + +# always return the aliasId so the generated file will be well-formed if not valid + $aliasId + + Write-Debug ""${debugPrefix} RESOURCE PROCESSING COMPLETED. TOTAL ERROR COUNT: $(Get-ConfigurationErrorCount)"" + + "; + } +} diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 95b26ff6907..a38bfa9218f 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -1721,7 +1721,10 @@ private static List GetResultForIdentifierInConfiguration( foreach (var keyword in matchedResults) { - string usageString = Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache.GetDSCResourceUsageString(keyword); + string usageString = Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache.NewApiIsUsed + ? Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache.GetDSCResourceUsageString(keyword) + : Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache.GetDSCResourceUsageString(keyword); + if (results == null) { results = new List(); diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index f1be0c5acc8..73cb117d687 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -123,6 +123,9 @@ static ExperimentalFeature() new ExperimentalFeature( name: "PSNotApplyErrorActionToStderr", description: "Don't have $ErrorActionPreference affect stderr output"), + new ExperimentalFeature( + name: "PS7DscSupport", + description: "Support the cross-platform class-based DSC"), new ExperimentalFeature( name: "PSSubsystemPluginModel", description: "A plugin model for registering and un-registering PowerShell subsystems"), diff --git a/src/System.Management.Automation/engine/LanguagePrimitives.cs b/src/System.Management.Automation/engine/LanguagePrimitives.cs index 293dbb6dce5..83361fe9662 100644 --- a/src/System.Management.Automation/engine/LanguagePrimitives.cs +++ b/src/System.Management.Automation/engine/LanguagePrimitives.cs @@ -1595,8 +1595,8 @@ public static string ConvertTypeNameToPSTypeName(string typeName) { "BooleanArray", "bool[]" }, { "UInt8Array", "byte[]" }, { "SInt8Array", "Sbyte[]" }, - { "UInt16Array", "uint16[]" }, - { "SInt16Array", "int64[]" }, + { "UInt16Array", "UInt16[]" }, + { "SInt16Array", "Int16[]" }, { "UInt32Array", "UInt32[]" }, { "SInt32Array", "Int32[]" }, { "UInt64Array", "UInt64[]" }, diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index 94ce2664703..9d5af609710 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -1014,7 +1014,7 @@ internal static string GetPSHomeModulePath() /// It's known as "Program Files" module path in windows powershell. /// /// - private static string GetSharedModulePath() + internal static string GetSharedModulePath() { #if UNIX return Platform.SelectProductNameForDirectory(Platform.XDG_Type.SHARED_MODULES); diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index cac1019ea67..5b8ca433454 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -12,6 +12,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Dsc = Microsoft.PowerShell.DesiredStateConfiguration.Internal; namespace System.Management.Automation.Language { @@ -2932,6 +2933,7 @@ private StatementAst ConfigurationStatementRule(IEnumerable custom // Runspaces.Runspace localRunspace = null; bool topLevel = false; + bool useCrossPlatformSchema = false; try { // At this point, we'll need a runspace to use to hold the metadata for the parse. If there is no @@ -2967,7 +2969,6 @@ private StatementAst ConfigurationStatementRule(IEnumerable custom ExpressionAst configurationBodyScriptBlock = null; - // Automatically import the PSDesiredStateConfiguration module at this point. PowerShell p = null; // Save the parser we're using so we can resume the current parse when we're done. @@ -2995,7 +2996,43 @@ private StatementAst ConfigurationStatementRule(IEnumerable custom { // Load the default CIM keywords Collection CIMKeywordErrors = new Collection(); - Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache.LoadDefaultCimKeywords(CIMKeywordErrors); + if (ExperimentalFeature.IsEnabled(Dsc.CrossPlatform.DscClassCache.DscExperimentalFeatureName)) + { + // In addition to checking if experimental feature is enabled + // also check if PSDesiredStateConfiguration is already loaded + // if pre-v3 is already loaded then use old mof-based APIs + // otherwise use json-based APIs + + p.AddCommand(new CmdletInfo("Get-Module", typeof(Microsoft.PowerShell.Commands.GetModuleCommand))); + p.AddParameter("Name", "PSDesiredStateConfiguration"); + + bool prev3IsLoaded = false; + foreach (PSModuleInfo moduleInfo in p.Invoke()) + { + if (moduleInfo.Version.Major < 3) + { + prev3IsLoaded = true; + break; + } + } + + p.Commands.Clear(); + + useCrossPlatformSchema = !prev3IsLoaded; + + if (useCrossPlatformSchema) + { + Dsc.CrossPlatform.DscClassCache.LoadDefaultCimKeywords(CIMKeywordErrors); + } + else + { + Dsc.DscClassCache.LoadDefaultCimKeywords(CIMKeywordErrors); + } + } + else + { + Dsc.DscClassCache.LoadDefaultCimKeywords(CIMKeywordErrors); + } // Report any errors encountered while loading CIM dynamic keywords. if (CIMKeywordErrors.Count > 0) @@ -3237,7 +3274,15 @@ private StatementAst ConfigurationStatementRule(IEnumerable custom // Clear out all of the cached classes and keywords. // They will need to be reloaded when the generated function is actually run. // - Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache.ClearCache(); + if (useCrossPlatformSchema) + { + Dsc.CrossPlatform.DscClassCache.ClearCache(); + } + else + { + Dsc.DscClassCache.ClearCache(); + } + System.Management.Automation.Language.DynamicKeyword.Reset(); } diff --git a/src/System.Management.Automation/resources/ParserStrings.resx b/src/System.Management.Automation/resources/ParserStrings.resx index f38b78f074b..dd9ce1b8c55 100644 --- a/src/System.Management.Automation/resources/ParserStrings.resx +++ b/src/System.Management.Automation/resources/ParserStrings.resx @@ -1452,6 +1452,12 @@ ModuleVersion : Version of module to import. If used, ModuleName must represent Conflict in using PsDscRunAsCredential for Resource {0} because it already specifies PsDscRunAsCredential value. We can only use one PsDscRunAsCredential for the composite resource. + + Unable to find DSC schema store at "{0}". Please ensure PSDesiredStateConfiguration v3 module is installed. + + + PS7DscSupport experimental feature is disabled; use Enable-ExperimentalFeature or update powershell.config.json to enable this feature. + {0} diff --git a/test/powershell/engine/ExperimentalFeature/Get-ExperimentalFeature.Tests.ps1 b/test/powershell/engine/ExperimentalFeature/Get-ExperimentalFeature.Tests.ps1 index bcc358bcccd..c11002fbedb 100644 --- a/test/powershell/engine/ExperimentalFeature/Get-ExperimentalFeature.Tests.ps1 +++ b/test/powershell/engine/ExperimentalFeature/Get-ExperimentalFeature.Tests.ps1 @@ -174,7 +174,14 @@ Describe "Default enablement of Experimental Features" -Tags CI { (Join-Path -Path $PSHOME -ChildPath 'powershell.config.json') | Should -Exist foreach ($expFeature in Get-ExperimentalFeature) { - $expFeature.Enabled | Should -BeEnabled -Name $expFeature.Name + if ($expFeature.Name -ne "PS7DscSupport") + { + $expFeature.Enabled | Should -BeEnabled -Name $expFeature.Name + } + else + { + $expFeature.Enabled | Should -Not -BeEnabled -Name $expFeature.Name + } } } }