From befc5f8ae100cd558150a2c70f33e10e65ccdeb5 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 17 Aug 2017 09:23:36 -0700 Subject: [PATCH] Make PowerShell Core enumerate COM collections (#4553) Make PowerShell Core enumerate COM collections --- .../engine/COM/ComTypeInfo.cs | 28 ++++-- .../engine/COM/ComUtil.cs | 86 ++++++++++++++++++- .../engine/runtime/Operations/MiscOps.cs | 27 +++--- .../powershell/engine/COM/COM.Basic.Tests.ps1 | 48 +++++++++++ 4 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 test/powershell/engine/COM/COM.Basic.Tests.ps1 diff --git a/src/System.Management.Automation/engine/COM/ComTypeInfo.cs b/src/System.Management.Automation/engine/COM/ComTypeInfo.cs index 28a881c458..971d83f89a 100644 --- a/src/System.Management.Automation/engine/COM/ComTypeInfo.cs +++ b/src/System.Management.Automation/engine/COM/ComTypeInfo.cs @@ -15,6 +15,18 @@ namespace System.Management.Automation /// internal class ComTypeInfo { + /// + /// A member with a DISPID equal to –4 is found on a collection interface. + /// This special member, often called '_NewEnum', returns an interface that enables clients to enumerate objects in a collection. + /// + internal const int DISPID_NEWENUM = -4; + + /// + /// A member with a DISPID equal to 0 is considered a default member. + /// Default members in COM can be transformed to default members in .NET (indexers in C#). + /// + internal const int DISPID_DEFAULTMEMBER = 0; + /// /// Member variables. /// @@ -44,7 +56,7 @@ namespace System.Management.Automation /// /// Collection of properties in the COM object. /// - public Dictionary Properties + internal Dictionary Properties { get { @@ -55,7 +67,7 @@ namespace System.Management.Automation /// /// Collection of methods in the COM object. /// - public Dictionary Methods + internal Dictionary Methods { get { @@ -63,12 +75,10 @@ namespace System.Management.Automation } } - - /// /// Returns the string of the GUID for the type information. /// - public string Clsid + internal string Clsid { get { @@ -76,6 +86,12 @@ namespace System.Management.Automation } } + /// + /// If 'DISPID_NEWENUM' member is present, return the InvokeKind; + /// otherwise, return null. + /// + internal COM.INVOKEKIND? NewEnumInvokeKind { get; private set; } + /// /// Initializes the typeinfo object /// @@ -91,6 +107,8 @@ namespace System.Management.Automation for (int i = 0; i < typeattr.cFuncs; i++) { COM.FUNCDESC funcdesc = GetFuncDesc(_typeinfo, i); + if (funcdesc.memid == DISPID_NEWENUM) { NewEnumInvokeKind = funcdesc.invkind; } + if ((funcdesc.wFuncFlags & 0x1) == 0x1) { // http://msdn.microsoft.com/en-us/library/ee488948.aspx diff --git a/src/System.Management.Automation/engine/COM/ComUtil.cs b/src/System.Management.Automation/engine/COM/ComUtil.cs index 9f30fd47ef..d77752abcc 100644 --- a/src/System.Management.Automation/engine/COM/ComUtil.cs +++ b/src/System.Management.Automation/engine/COM/ComUtil.cs @@ -5,6 +5,7 @@ Copyright (c) Microsoft Corporation. All rights reserved. using System.Runtime.InteropServices; using System.Management.Automation.ComInterop; using System.Text; +using System.Collections; using System.Collections.ObjectModel; using COM = System.Runtime.InteropServices.ComTypes; @@ -355,4 +356,87 @@ namespace System.Management.Automation return returnValue; } } -} \ No newline at end of file + + /// + /// Defines an enumerator that represent a COM collection object. + /// + internal class ComEnumerator : IEnumerator + { + private COM.IEnumVARIANT _enumVariant; + private object[] _element; + + private ComEnumerator(COM.IEnumVARIANT enumVariant) + { + _enumVariant = enumVariant; + _element = new object[1]; + } + + public object Current + { + get { return _element[0]; } + } + + public bool MoveNext() + { + _element[0] = null; + int result = _enumVariant.Next(1, _element, IntPtr.Zero); + return result == 0; + } + + public void Reset() + { + _element[0] = null; + _enumVariant.Reset(); + } + + /// + /// Try to create an enumerator for a COM object. + /// + /// + /// A 'ComEnumerator' instance, or null if we cannot create an enumerator for the COM object. + /// + internal static ComEnumerator Create(object comObject) + { + if (comObject == null || !comObject.GetType().IsCOMObject) { return null; } + + // The passed-in COM object could already be a IEnumVARIANT interface. + // e.g. user call '_NewEnum()' on a COM collection interface. + var enumVariant = comObject as COM.IEnumVARIANT; + if (enumVariant != null) + { + return new ComEnumerator(enumVariant); + } + + // The passed-in COM object could be a collection. + var enumerable = comObject as IEnumerable; + var target = comObject as IDispatch; + if (enumerable != null && target != null) + { + try + { + var comTypeInfo = ComTypeInfo.GetDispatchTypeInfo(comObject); + if (comTypeInfo != null && comTypeInfo.NewEnumInvokeKind.HasValue) + { + // The COM object is a collection and also a IDispatch interface, so we try to get a + // IEnumVARIANT interface out of it by invoking its '_NewEnum (DispId: -4)' function. + var result = ComInvoker.Invoke(target, ComTypeInfo.DISPID_NEWENUM, + args: Utils.EmptyArray(), byRef: null, + invokeKind: comTypeInfo.NewEnumInvokeKind.Value); + enumVariant = result as COM.IEnumVARIANT; + if (enumVariant != null) + { + return new ComEnumerator(enumVariant); + } + } + } + catch (Exception) + { + // Ignore exceptions. In case of exception, no enumerator can be created + // for the passed-in COM object, and we will return null eventually. + } + } + + return null; + } + } +} diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs index 80e2effc1a..d5ab00d938 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs @@ -3199,23 +3199,18 @@ namespace System.Management.Automation internal static IEnumerator GetCOMEnumerator(object obj) { object targetValue = PSObject.Base(obj); - try - { - IEnumerable enumerable = targetValue as IEnumerable; - if (enumerable != null) - { - var enumerator = enumerable.GetEnumerator(); - if (enumerator != null) - { - return enumerator; - } - } - } - catch (Exception) - { - } - return targetValue as IEnumerator ?? NonEnumerableObjectEnumerator.Create(obj); + // We use ComEnumerator to enumerate COM collections because the following code doesn't work in .NET Core + // IEnumerable enumerable = targetValue as IEnumerable; + // if (enumerable != null) + // { + // var enumerator = enumerable.GetEnumerator(); + // ... + // } + // The call to 'GetEnumerator()' throws exception because COM is not supported in .NET Core. + // See https://github.com/dotnet/corefx/issues/19731 for more information. + // When COM support is back to .NET Core, we need to change back to the original implementation. + return ComEnumerator.Create(targetValue) ?? NonEnumerableObjectEnumerator.Create(obj); } internal static IEnumerator GetGenericEnumerator(IEnumerable enumerable) diff --git a/test/powershell/engine/COM/COM.Basic.Tests.ps1 b/test/powershell/engine/COM/COM.Basic.Tests.ps1 new file mode 100644 index 0000000000..f98139f9b8 --- /dev/null +++ b/test/powershell/engine/COM/COM.Basic.Tests.ps1 @@ -0,0 +1,48 @@ + +try { + $defaultParamValues = $PSdefaultParameterValues.Clone() + $PSDefaultParameterValues["it:skip"] = ![System.Management.Automation.Platform]::IsWindowsDesktop + + Describe 'Basic COM Tests' -Tags "CI" { + BeforeAll { + $null = New-Item -Path $TESTDRIVE/file1 -ItemType File + $null = New-Item -Path $TESTDRIVE/file2 -ItemType File + $null = New-Item -Path $TESTDRIVE/file3 -ItemType File + } + + It "Should enumerate files from a folder" { + $shell = New-Object -ComObject "Shell.Application" + $folder = $shell.Namespace("$TESTDRIVE") + $items = $folder.Items() + + ## $items is a collection of all items belong to the folder, and it should be enumerated. + $items | Measure-Object | ForEach-Object Count | Should Be $items.Count + } + + It "Should enumerate IEnumVariant interface object without exception" { + $shell = New-Object -ComObject "Shell.Application" + $folder = $shell.Namespace("$TESTDRIVE") + $items = $folder.Items() + + ## $enumVariant is an IEnumVariant interface of all items belong to the folder, and it should be enumerated. + $enumVariant = $items._NewEnum() + $enumVariant | Measure-Object | ForEach-Object Count | Should Be $items.Count + } + + It "Should enumerate drives" { + $fileSystem = New-Object -ComObject scripting.filesystemobject + $drives = $fileSystem.Drives + + ## $drives is a read-only collection of all available drives, and it should be enumerated. + $drives | Measure-Object | ForEach-Object Count | Should Be $drives.Count + ## $element should be the first drive from the enumeration. It shouldn't be the same as $drives, + ## but it should be the same as '$drives.Item($element.DriveLetter)' + $element = $drives | Select-Object -First 1 + [System.Object]::ReferenceEquals($element, $drives) | Should Be $false + $element | Should Be $drives.Item($element.DriveLetter) + } + } + +} finally { + $global:PSdefaultParameterValues = $defaultParamValues +}