Skip to content
28 changes: 23 additions & 5 deletions src/System.Management.Automation/engine/COM/ComTypeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ namespace System.Management.Automation
/// </summary>
internal class ComTypeInfo
{
/// <summary>
/// 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.
/// </summary>
internal const int DISPID_NEWENUM = -4;

/// <summary>
/// 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#).
/// </summary>
internal const int DISPID_DEFAULTMEMBER = 0;

/// <summary>
/// Member variables.
/// </summary>
Expand Down Expand Up @@ -44,7 +56,7 @@ internal ComTypeInfo(COM.ITypeInfo info)
/// <summary>
/// Collection of properties in the COM object.
/// </summary>
public Dictionary<String, ComProperty> Properties
internal Dictionary<String, ComProperty> Properties
{
get
{
Expand All @@ -55,27 +67,31 @@ public Dictionary<String, ComProperty> Properties
/// <summary>
/// Collection of methods in the COM object.
/// </summary>
public Dictionary<String, ComMethod> Methods
internal Dictionary<String, ComMethod> Methods
{
get
{
return _methods;
}
}



/// <summary>
/// Returns the string of the GUID for the type information.
/// </summary>
public string Clsid
internal string Clsid
{
get
{
return _guid.ToString();
}
}

/// <summary>
/// If 'DISPID_NEWENUM' member is present, return the InvokeKind;
/// otherwise, return null.
/// </summary>
internal COM.INVOKEKIND? NewEnumInvokeKind { get; private set; }

/// <summary>
/// Initializes the typeinfo object
/// </summary>
Expand All @@ -91,6 +107,8 @@ private void Initialize()
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
Expand Down
86 changes: 85 additions & 1 deletion src/System.Management.Automation/engine/COM/ComUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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;

Expand Down Expand Up @@ -355,4 +356,87 @@ internal static ComMethodInformation[] GetMethodInformationArray(COM.ITypeInfo t
return returnValue;
}
}
}

/// <summary>
/// Defines an enumerator that represent a COM collection object.
/// </summary>
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();
}

/// <summary>
/// Try to create an enumerator for a COM object.
/// </summary>
/// <returns>
/// A 'ComEnumerator' instance, or null if we cannot create an enumerator for the COM object.
/// </returns>
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<object>(), 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3199,23 +3199,18 @@ internal object GetNonEnumerableObject()
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<T>(IEnumerable<T> enumerable)
Expand Down
48 changes: 48 additions & 0 deletions test/powershell/engine/COM/COM.Basic.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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
}