Skip to content

Code OK on V5.1 does not run on V7.0.0RC2 #11782

Closed

Description

@RG255
Name                           Value
----                           -----
PSVersion                      7.0.0-rc.2
PSEdition                      Core
GitCommitId                    7.0.0-rc.2
OS                             Microsoft Windows 10.0.18363
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

The code in the attached script runs OK on V5 but generates the following error on V7:

MethodInvocationException: S:\PowerShellScripts\TestUserIngroup_1.ps1:5
Line |
   5 |  … t -Process {$_.GetType().InvokeMember('Name', 'GetProperty', $null, $ …
     |                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Exception calling "InvokeMember" with "5" argument(s): "Unknown name. (0x80020006 (DISP_E_UNKNOWNNAME))"

The script:

$user = $env:USERNAME
$group = 'Users'
$groupObj = [ADSI]"WinNT://./$group,group"
$membersObj = @($groupObj.psbase.Invoke('Members'))
$members = ($membersObj | ForEach-Object -Process {$_.GetType().InvokeMember('Name', 'GetProperty', $null, $_, $null)})

If ($members -contains $user) 
{Write-Host -Object "$user exists in the group $group"}
Else 
{Write-Host -Object "$user does not exists in the group $group"}

TestUserIngroup_1.zip

Activity

  1. added
    Issue-Questionideally support can be provided via other mechanisms, but sometimes folks do open an issue to get a
    on Feb 5, 2020
  2. iSazonov commented on Feb 6, 2020

    @iSazonov
    Collaborator
  3. RG255 commented on Feb 6, 2020

    @RG255
    Author

    I am somewhat new to GitHub so I am not sure what you are expecting when you say "Have you any thoughts" My only thought at this time would be expecting it to work on PSV7* or there to be a better/alternative way to achieve the same result?

  4. iSazonov commented on Feb 6, 2020

    @iSazonov
    Collaborator

    RG255 References like "RG255" means contacting the owner of this name. So my question was for Patrick Meinecke (@SeeminglyScience) who could share useful thoughts about the issue.
    You could look other issues/discussion in the repo to get more experience about workflows.

  5. SeeminglyScience commented on Feb 6, 2020

    @SeeminglyScience
    Contributor

    Ilya (@iSazonov) Looks like PowerShell isn't able to determine that IADsMembers (the collection returned by Members) is enumerable. So instead of $membersObj enumerating into member objects like in Windows PowerShell, the attempt is made to call a Name property on the IADsMembers interface which doesn't exist.

    You can kinda see this by attempting to get the property Count instead, which will throw not implemented instead of unknown name.

  6. SeeminglyScience commented on Feb 6, 2020

    @SeeminglyScience
    Contributor

    So playing with it a bit, my guess is that this happens because if you query IDispatch.GetIDsOfNames with the names _NewEnum and ppEnumerator then that exception is thrown. However, if you manually invoke GetIDsOfNames with a preserved sig (no automatic HResult handling) you'll see the HResult above returned, but it'll also show DISPID_NEWENUM for _NewEnum. If _NewEnum was really not found, it would be -1.

    Not sure atm where this needs to be fixed though.

    Tested with this code, click to expand
    Add-Type -TypeDefinition '
        using System;
        using System.Runtime.InteropServices;
        using System.Runtime.InteropServices.ComTypes;
    
        namespace Testing
        {
            public class DispatchWrapper
            {
                private readonly IDispatch _real;
    
                internal DispatchWrapper(IDispatch real) => _real = real;
    
                public static DispatchWrapper Create(object obj) => new DispatchWrapper((IDispatch)obj);
    
                public int GetTypeInfoCount(out int info) => _real.GetTypeInfoCount(out info);
    
                public int GetTypeInfo(int iTInfo, int lcid, out ITypeInfo ppTInfo) => _real.GetTypeInfo(iTInfo, lcid, out ppTInfo);
    
                public int GetIDsOfNames(Guid iid, string[] rgszNames, int cNames, int lcid, int[] rgDispId)
                    => _real.GetIDsOfNames(iid, rgszNames, cNames, lcid, rgDispId);
    
                public void Invoke(
                    int dispIdMember,
                    Guid iid,
                    int lcid,
                    INVOKEKIND wFlags,
                    DISPPARAMS[] paramArray,
                    out object pVarResult,
                    out EXCEPINFO pExcepInfo,
                    out uint puArgErr)
                    => _real.Invoke(
                        dispIdMember,
                        iid,
                        lcid,
                        wFlags,
                        paramArray,
                        out pVarResult,
                        out pExcepInfo,
                        out puArgErr);
            }
    
            [Guid("00020400-0000-0000-c000-000000000046")]
            [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
            [ComImport]
            internal interface IDispatch
            {
                [PreserveSig]
                int GetTypeInfoCount(out int info);
    
                [PreserveSig]
                int GetTypeInfo(int iTInfo, int lcid, out ITypeInfo ppTInfo);
    
                [PreserveSig]
                int GetIDsOfNames(
                    [MarshalAs(UnmanagedType.LPStruct)] Guid iid,
                    [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr)] string[] rgszNames,
                    int cNames,
                    int lcid,
                    [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.I4)] [Out] int[] rgDispId);
    
                void Invoke(
                    int dispIdMember,
                    [MarshalAs(UnmanagedType.LPStruct)] Guid iid,
                    int lcid,
                    INVOKEKIND wFlags,
                    [MarshalAs(UnmanagedType.LPArray)] [In] [Out] DISPPARAMS[] paramArray,
                    out object pVarResult,
                    out EXCEPINFO pExcepInfo,
                    out uint puArgErr);
            }
        }'
    
    $group = 'Users'
    $groupObj = [ADSI]"WinNT://./$group,group"
    $membersObj = @($groupObj.psbase.Invoke('Members'))
    $disp = [Testing.DispatchWrapper]::Create($membersObj[0])
    $nullGuid = [activator]::CreateInstance([guid])
    $enUS = [cultureinfo]::CurrentCulture.LCID
    $namesToQuery = '_NewEnum', 'ppEnumerator'
    $dispIds = [int[]]::new($namesToQuery.Length)
    $hr = $disp.GetIDsOfNames($nullGuid, $namesToQuery, $namesToQuery.Length, $enUS, $dispIds)
    
    'HResult: 0x{0:X}' -f $hr
    for ($i = 0; $i -lt $namesToQuery.Length; $i++) {
        '{0} ID: {1}' -f $namesToQuery[$i], $dispIds[$i]
    }

    Should return:

    HResult: 0x80020006
    DispIDs: -4, -1
    
  7. SeeminglyScience commented on Feb 6, 2020

    @SeeminglyScience
    Contributor

    Another update, here's the code that needs to change:

    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: Array.Empty<object>(), byRef: null,
    invokeKind: comTypeInfo.NewEnumInvokeKind.Value);
    enumVariant = result as COM.IEnumVARIANT;
    if (enumVariant != null)
    {
    return new ComEnumerator(enumVariant);
    }
    }

    It fails because IADsMembers implements IDispatch, but throws E_NOTIMPL when trying to get type info. The other members work fine though.

    Instead of (or in addition to) querying type info, it needs to directly invoke GetIDsOfNames with two names. The first has to be _NewEnum and the second can apparently be anything. It needs two though, otherwise it returns unknown. If the first ID is DISPID_NEWENUM then _NewEnum can be invoked. The GetIDsOfNames implementation needs PreserveSig and it's return type changed to int. If DISPID_NEWENUM is found and the HResult is DISP_E_UNKNOWNNAME, then the HResult should be ignored.

    Example invoke using the above code in the details pane:

    $results = $null
    $excep = [Activator]::CreateInstance([Runtime.InteropServices.ComTypes.EXCEPINFO])
    $argErr = 0u;
    $disp.Invoke(-4, $nullGuid, $enUS, 'INVOKE_PROPERTYGET', @(), [ref] $res, [ref] $excep, [ref] $argErr)
    
    # yield
    $results

    Which returns:

    System.__ComObject
    System.__ComObject
    System.__ComObject
    System.__ComObject
    System.__ComObject
    System.__ComObject
    
  8. SeeminglyScience commented on Feb 6, 2020

    @SeeminglyScience
    Contributor

    /cc Steve Lee (@SteveL-MSFT) I'm pretty sure I've seen quite a few folks on with similar issues on the PS slack. This might be the first issue for it here, but it's potentially very impactful for folks still using COM objects frequently (and who aren't super likely to be vocal here). If possible, it may be worth resolving before 7.0 GA.

  9. iSazonov commented on Feb 6, 2020

    @iSazonov
    Collaborator

    Patrick Meinecke (@SeeminglyScience) Thanks for your investigations!
    I think we need to have more tests for COM interactions.

  10. added
    WG-Enginecore PowerShell engine, interpreter, and runtime
    Issue-Enhancementthe issue is more of a feature request than a bug
    and removed
    Issue-Questionideally support can be provided via other mechanisms, but sometimes folks do open an issue to get a
    on Feb 6, 2020
  11. daxian-dbw commented on Feb 7, 2020

    @daxian-dbw
    Member

    Thanks Patrick Meinecke (@SeeminglyScience) for analyzing the issue and pinpoint the cause ❤️

    The ComEnumerator was introduced back in early .NET Core 2.0 preview period of time (#4553), because GetEnumerator() didn't work on COM object even if the object can be cast to IEnumerable and .NET Core team said it was by design at the time (dotnet/runtime#21690).
    With .NET Core 3.1, GetEnumerator() works on the COM objects that can be cast to IEnumerable 🎉

    However, for the COM object that can be cast to IEnumerator, exception is thrown when calling MoveNext() on it.
    So we still need the ComEnumerator, but much simplified to just cover the case where the COM object implements COM.IEnumVARIANT interface.

    PR was submitted: #11795

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Resolution-FixedThe issue is fixed.WG-Enginecore PowerShell engine, interpreter, and runtime

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions